前言
参考 @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启动项目
springboot项目结构
参考 https://s31k31.github.io/2020/04/26/JavaSpringBootCodeAudit-2-SpringBoot/
SQL注入
该项目GitHub中的第一个issue就是有关 SQL 注入的漏洞的 https://github.com/newbee-ltd/newbee-mall/issues/1
项目的搜索框中输入 1'
发现报错
回到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
可以发现回显了所有的商品
使用sqlmap也能轻松检测出来
导致sql注入的原因
在Mybatis的配置 NewBeeMallGoodsMapper.xml 中发现
这里获取到用户传入的 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
第一处
首先黑盒测试下
搜索框这里并没有XSS,到源码中去看看
可以看到后端并没有对传入的 keyword 做任何处理。但确实是没有XSS。同时审计其他地方也未发现有任何的过滤或替换。但这里没有XSS成功,原因是项目使用了 thymeleaf 模板来渲染,模板自带有字符转义的功能。
这里对搜索的字符串显示使用了 th:text
输出的是转义后的字符串。
想要得到未经过模板转义后的字符串通过 th:utext
来实现,全局搜索下
找到了这两处
第一处在 detail.html 中。是显示商品的介绍。
这里只能通过admin面板的商品介绍处修改
抓包修改标签
前台该商品的介绍成功触发XSS
第二处
在 newbee_mall_goods_edit.html 中也有一处 thymeleaf 的 th:utext
的标签,是编辑商品介绍这里
这一处和上面是一样的,都是商品介绍 这部分的XSS(都需要管理员操作才行,危害不大)
第三处
继续黑盒测试,尝试修改个人订单中的收货地址并提交订单
当前页面没有效果,到管理员面板的订单管理页面发现
可以获取到管理员的cookie。危害较大。
具体查看源码可以发现这部分收货地址的显示是收到可一份json数据,然后前端将数据写入到浏览器html页面,导致xss。后端未对用户传入的收货地址信息进行合法性校验。
本项目还有其他基础XSS,就不依次列举了
越权
该项目是使用 interceptor(拦截器)根据URL路径做访问控制的 具体参考 https://s31k31.github.io/2020/05/04/JavaSpringBootCodeAudit-5-IDOR/ 笔者只是参考这位师傅复现。
springMVC的工作流程
首先看他的拦截器是如何处理的
Interceptor配置在/src/main/java/ltd/newbee/mall/config/NeeBeeMallWebMvcConfigurer.java
中,针对url路径设置了不同的interceptor。
addPathPatterns
表示其中的路径会经过设置的拦截器,excludePathPatterns
则不过该拦截器。其中两个星**
表示匹配任意字符,如果出现一个*
则表示匹配单个路径
Spring中拦截器的分配是由DispatcherServlet
来分配的,也就是根据ServletPath
来分配路径,和getRequestURI
无关,也就是不管怎么使用../
来做路径穿越,最终得到的还是ServletPath。如果使用类似%00
空字符进行截断路径,会使得DispatcherServlet
无法将请求分配到正确的Controller,导致请求无效。
所以后面对请求的路由进行判断是应该使用 getServletPath()
来获取最后真正分发到路由地方的 path
getRequestURI()
只是获取了请求的 URI
而该项目对 admin 的拦截判断是这样处理的
使用了 getRequestURI()
来获取URI来判断URI是否以 /admin
开头,如果是 /admin
开头则校验 session,不是则不用校验。
这里存在很明显的越权漏洞,参考 https://joychou.org/web/security-of-getRequestURI.html
可以使用 //admin
或 /index/..;/admin
直接越权成为管理员。
ps:目前较新版的spring和tomcat已经修改了对 /
;
等特殊字符的匹配规则,该漏洞在新版中可能无法利用。
简单调试下看看他是怎么个回事。
可以看到访问过去取到的 URI 是 //admin
,不满足以 /admin
开头的条件,直接返回 true 了。上面也提到过spring根据ServletPath
来分配路径,tomcat 服务器在解析 ServletPath
时会对 /
;
等特殊字符进行处理。
像上面的 //admin
经过处理后其ServletPath
为 /admin
,那么直接就去找 /admin
的路由了,同时拦截器也给出的时 true ,从而造成越权。实际上这个越权漏洞就是开发者使用的函数不当,造成和服务器解析差,从而产生问题。
水平越权
审计代码,在修改用户信息这里
ltd/newbee/mall/controller/mall/PersonalController.java:114
查看下 updateUserInfo()
的实现,
ltd/newbee/mall/service/impl/NewBeeMallUserServiceImpl.java:72
发现这里从数据库查找用户并未用到 session ,而是直接以传递过来的 userId 为参数来查找并修改数据的,所以这里存在水平越权漏洞,修改 userId 便可修改其他用户的信息。
目标用户 userId 为 11
我们通过另一个用户来修改 userId为 11的用户信息
可以看到成功修改了 userId 为11的用户的信息。
另一处在查询订单这部分。
ltd/newbee/mall/controller/mall/OrderController.java:36
这里我们跟进看下 getOrderDetailByOrderNo()
的实现
ltd/newbee/mall/service/impl/NewBeeMallOrderServiceImpl.java:252
可以发现这里数据库查询所使用的 订单号 并不是上面通过session获得的订单号,而用的是用户传入的 orderNo。
这里用户 userId 为11 的用户下的单
其他用户也可以直接访问
CSRF 和 逻辑漏洞
笔者已复现,懒得写了。反正也是抄来的
参考
https://s31k31.github.io/2020/05/05/JavaSpringBootCodeAudit-6-CSRF/
https://s31k31.github.io/2020/05/06/JavaSpringBootCodeAudit-7-Logical-Vulnerability/