前言

参考 @s31k3 师傅的 java SpringBoot框架代码审计 ,本文仅复现这位师傅的教程,用于学习springboot代码审计,特此笔记,原文请关注 @s31k3

环境搭建

审计的项目是github上 9.5k start的开源项目 newbee-mall。由于最新版的项目已修复多个漏洞,本文使用的是 Oct 17, 2019 的版本,项目地址 https://github.com/newbee-ltd/newbee-mall/tree/36807c87d13ee9ca08aff75197063b8836d8711d

基础配置

使用IntelliJ IDEA打开项目文件夹,配置好SDK后IDEA会以maven项目打开并自动下载依赖包。

Spring属性文件路径:/src/main/resources/application.properties,其中可修改端口和mysql数据库地址

server.port=8089
...
spring.datasource.url=jdbc:mysql://localhost:3306/newbee_mall_db?...

配置文件路径:/src/main/java/ltd/newbee/mall/config/NeeBeeMallWebMvcConfigurer.java,其中配置了图片路径

public void addResourceHandlers(ResourceHandlerRegistry registry) {
	registry.addResourceHandler("/upload/**").addResourceLocations("file:" + Constants.FILE_UPLOAD_DIC);
	registry.addResourceHandler("/goods-img/**").addResourceLocations("file:" + Constants.FILE_UPLOAD_DIC);
}

此时是没有测试数据的,需要将/src/main/resources/upload.zip压缩包中的测试商品数据解压出来,放到任意的目录中。此处作为学习测试使用,可以直接解压在当前路径下,正式系统中一定要存放在非项目路径下。

在文件src/main/java/ltd/newbee/mall/common/Constants.java中,变量FILE_UPLOAD_DIC为当前上传图片路径,将其更改为我们解压upload.zip绝对路径

public class Constants {
	//上传文件的默认url前缀,根据部署设置自行修改
	public final static String FILE_UPLOAD_DIC = "/some_path/src/main/resources/upload/";
	//public final static String FILE_UPLOAD_DIC = "D:\\upload\\";//上传文件的默认url前缀,根据部署设置自行修改
}

数据库配置

/src/main/resources 目录下有数据库文件 newbee_mall_schema.sql 。利用该文件创建数据库。

笔者的数据库是用直接docker pull下来的。

docker cp ./newbee_mall_schema.sql container_id:/root

(container_id为mysql容器名)

进入MYSQL容器创建数据库

root:/# mysql -u root -p
mysql> create database newbee_mall_db;
mysql> exit

执行sql文件

root:/# mysql -u root -p newbee_mall_db</root/newbee_mall_schema.sql

此时数据库成功配置完毕

启动项目

如上文配置完毕后即可使用IDEA启动项目

image-20221118211239599

springboot项目结构

参考 https://s31k31.github.io/2020/04/26/JavaSpringBootCodeAudit-2-SpringBoot/

request-path

request-example

SQL注入

该项目GitHub中的第一个issue就是有关 SQL 注入的漏洞的 https://github.com/newbee-ltd/newbee-mall/issues/1

项目的搜索框中输入 1' 发现报错

image-20221118212438097

回到IDEA中查看报错信息

### The error may involve ltd.newbee.mall.dao.NewBeeMallGoodsMapper.findNewBeeMallGoodsListBySearch-Inline
### The error occurred while setting parameters
### SQL: select               goods_id, goods_name, goods_intro,goods_category_id, goods_cover_img, goods_carousel, original_price,     selling_price, stock_num, tag, goods_sell_status, create_user, create_time, update_user,      update_time             from tb_newbee_mall_goods_info          WHERE  (goods_name like CONCAT('%','1'','%') or goods_intro like CONCAT('%','1'','%'))                                  limit ?,?
### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '1'','%')) 
         
         
            limit 0,10' at line 8
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '1'','%')) 
         
         
            limit 0,10' at line 8] with root cause

java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '1'','%')) 
         
         
            limit 0,10' at line 8

根据报错信息可以得到这样的payload

/search?keyword=1')) OR 1%23

可以发现回显了所有的商品

image-20221118212832929

使用sqlmap也能轻松检测出来

image-20221118213033306

导致sql注入的原因

在Mybatis的配置 NewBeeMallGoodsMapper.xml 中发现

image-20221118214431414

这里获取到用户传入的 keyword 后使用了 ${keyword} 来接收用户传来的参数。而${} 仅仅是纯粹的 string 替换,在动态 SQL 解析阶段将会进行变量替换,类似于直接替换字符串,会导致SQL注入产生。

MyBatis官方文档中有如下叙述:

#{}告诉 MyBatis 创建一个预编译语句(PreparedStatement)参数,在 JDBC 中,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中,就像这样:

// 近似的 JDBC 代码,非 MyBatis 代码...
String selectPerson = "SELECT * FROM PERSON WHERE ID=?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);

同时官方文档也对 ${} 给出警示:

提示 用这种方式接受用户的输入,并用作语句参数是不安全的,会导致潜在的 SQL 注入攻击。因此,要么不允许用户输入这些字段,要么自行转义并检验这些参数。

有关更多 Mybatis 的sql注入参考 https://s31k31.github.io/2020/05/01/JavaSpringBootCodeAudit-3-SQL-Injection/#SQL%E6%B3%A8%E5%85%A5%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1

XSS

第一处

首先黑盒测试下

image-20221119141854294

搜索框这里并没有XSS,到源码中去看看

image-20221119142110556

可以看到后端并没有对传入的 keyword 做任何处理。但确实是没有XSS。同时审计其他地方也未发现有任何的过滤或替换。但这里没有XSS成功,原因是项目使用了 thymeleaf 模板来渲染,模板自带有字符转义的功能。

这里对搜索的字符串显示使用了 th:text

image-20221119142602873

输出的是转义后的字符串。

想要得到未经过模板转义后的字符串通过 th:utext 来实现,全局搜索下

找到了这两处

image-20221119142840555

第一处在 detail.html 中。是显示商品的介绍。

这里只能通过admin面板的商品介绍处修改

image-20221119143709264

抓包修改标签

image-20221119143830321

前台该商品的介绍成功触发XSS

image-20221119143928286

第二处

在 newbee_mall_goods_edit.html 中也有一处 thymeleaf 的 th:utext 的标签,是编辑商品介绍这里

这一处和上面是一样的,都是商品介绍 这部分的XSS(都需要管理员操作才行,危害不大)

第三处

继续黑盒测试,尝试修改个人订单中的收货地址并提交订单

image-20221119151806853

当前页面没有效果,到管理员面板的订单管理页面发现

1111

可以获取到管理员的cookie。危害较大。

具体查看源码可以发现这部分收货地址的显示是收到可一份json数据,然后前端将数据写入到浏览器html页面,导致xss。后端未对用户传入的收货地址信息进行合法性校验。

image-20221119153956250

本项目还有其他基础XSS,就不依次列举了

越权

该项目是使用 interceptor(拦截器)根据URL路径做访问控制的 具体参考 https://s31k31.github.io/2020/05/04/JavaSpringBootCodeAudit-5-IDOR/ 笔者只是参考这位师傅复现。

springMVC的工作流程

img

首先看他的拦截器是如何处理的

Interceptor配置在/src/main/java/ltd/newbee/mall/config/NeeBeeMallWebMvcConfigurer.java中,针对url路径设置了不同的interceptor。

addPathPatterns表示其中的路径会经过设置的拦截器,excludePathPatterns则不过该拦截器。其中两个星**表示匹配任意字符,如果出现一个*则表示匹配单个路径

image-20221119160109414

Spring中拦截器的分配是由DispatcherServlet来分配的,也就是根据ServletPath来分配路径,和getRequestURI无关,也就是不管怎么使用../来做路径穿越,最终得到的还是ServletPath。如果使用类似%00空字符进行截断路径,会使得DispatcherServlet无法将请求分配到正确的Controller,导致请求无效。

所以后面对请求的路由进行判断是应该使用 getServletPath() 来获取最后真正分发到路由地方的 path

getRequestURI() 只是获取了请求的 URI

而该项目对 admin 的拦截判断是这样处理的

image-20221119160438878

使用了 getRequestURI() 来获取URI来判断URI是否以 /admin 开头,如果是 /admin 开头则校验 session,不是则不用校验。

这里存在很明显的越权漏洞,参考 https://joychou.org/web/security-of-getRequestURI.html

可以使用 //admin/index/..;/admin 直接越权成为管理员。

image-20221119160924071

image-20221119160939718

ps:目前较新版的spring和tomcat已经修改了对 / ; 等特殊字符的匹配规则,该漏洞在新版中可能无法利用。

简单调试下看看他是怎么个回事。

image-20221119161326175

可以看到访问过去取到的 URI 是 //admin ,不满足以 /admin 开头的条件,直接返回 true 了。上面也提到过spring根据ServletPath来分配路径,tomcat 服务器在解析 ServletPath 时会对 / ; 等特殊字符进行处理。

image-20221119162702611

像上面的 //admin 经过处理后其ServletPath/admin,那么直接就去找 /admin 的路由了,同时拦截器也给出的时 true ,从而造成越权。实际上这个越权漏洞就是开发者使用的函数不当,造成和服务器解析差,从而产生问题。

水平越权

审计代码,在修改用户信息这里

ltd/newbee/mall/controller/mall/PersonalController.java:114

image-20221119170701015

查看下 updateUserInfo() 的实现,

ltd/newbee/mall/service/impl/NewBeeMallUserServiceImpl.java:72

image-20221119171227788

发现这里从数据库查找用户并未用到 session ,而是直接以传递过来的 userId 为参数来查找并修改数据的,所以这里存在水平越权漏洞,修改 userId 便可修改其他用户的信息。

目标用户 userId 为 11

image-20221119172033334

我们通过另一个用户来修改 userId为 11的用户信息

image-20221119172240231

image-20221119172315856

可以看到成功修改了 userId 为11的用户的信息。

另一处在查询订单这部分。

ltd/newbee/mall/controller/mall/OrderController.java:36

image-20221119173329462

这里我们跟进看下 getOrderDetailByOrderNo() 的实现

ltd/newbee/mall/service/impl/NewBeeMallOrderServiceImpl.java:252

image-20221119173641451

可以发现这里数据库查询所使用的 订单号 并不是上面通过session获得的订单号,而用的是用户传入的 orderNo。

这里用户 userId 为11 的用户下的单

image-20221119173943212

其他用户也可以直接访问

image-20221119174129604

CSRF 和 逻辑漏洞

笔者已复现,懒得写了。反正也是抄来的

参考

https://s31k31.github.io/2020/05/05/JavaSpringBootCodeAudit-6-CSRF/

https://s31k31.github.io/2020/05/06/JavaSpringBootCodeAudit-7-Logical-Vulnerability/