Java代码审计实战-1
本文最后更新于:2025年6月29日 上午
Java代码审计:奇文网盘-SQL注入漏洞(CVE-2024-50942)
A SQLInjection In Qiwen-file
环境搭建
应用官网:奇文社区,存在许多开源软件
Github寻找开源仓库,奇文网盘
直接git clone整个项目到本地,然后用IDEA打开,等待配置加载完成即可。该项目使用了MySQL数据库,通过JPA在程序运行时自动创建数据库。首先确保项目能够运行,可能会出现一些报错,比如文件编码、jdk版本、配置文件、依赖包未导入等待问题,需要自己解决。

这里我只导入了其后端项目,没有必要运行前端,直接测试接口即可。
审计
对于一个采用Maven打包的项目来说,首先应该看pom.xml
文件,是否存在不安全的依赖包。
存在mybatis-plus
依赖,找一找是否存在SQL注入,关键字$
,漏洞原理这里不提,自行了解。

IDEA全局搜索:Edit->Find->Find in Files

查找到唯一一处存在$
且在.xml映射文件中的地方,

那就是从这里入手,分析一下后端处理逻辑:
控制层:/notice/list
,GET方法,传参的话可以只填title

服务层:selectUserPage
,写的很简单,没有进行过滤,直接将传输对象作为查询内容,MyBatis会自动映射字段

下面进行注入测试,在配置文件中添加调试输出:
1 |
|
然后构造请求包,先注入一个'
1 |
|
服务端爆500,

调试日志中会输出预编译的语句和报错信息:
可以发现确实存在SQL注入,已经使用编译完后的语句去访问数据库才报的错

在发现SQL注入点时,为了节省时间,一般选用sqlmap一把梭,但是这里sqlmap打不出来,所有我采用了手注。
使用payload:'--
,未报错:

'union/**/select/**/database()--
,报错,发现SQL执行语句发生了变化,可能是使用了分页查找,这里统计个数,看报错信息,少了一个)
,

加上,还是报错,拿去SQL脚本执行一下,这里采用了多级查询,多级查询的过程中,会需要给表一个别名

改造payload:)
换个位置,然后取个别名
')as/**/total/**/union/**/select/**/database()--
此时完整的sql语句为:
1 |
|
已经在sql脚本中可以正常执行:

但是在服务端中还是无法正常执行,这就很奇怪,已经输出了正常的预编译语句,在sql脚本中也可以执行,那问题出在哪

思考一下,什么情况下-- %' order by createTime desc
编译器认为没错,)TOTAL
有错呢,不妨看看下面这样:
对,就是换行,所以现在情况是无论如何,这个sql语句都会报错,因为我们无法控制写死的换行。

那接下来怎么办呢,其实很好解决,想一想,平时在mysql中写sql语句的时候,你不小心输入了一个换行符,是不是并没有立即执行,需要一个;
来结尾,这个sql语句才算写完了。那么在这里,添加一个;
,-- %' order by createTime desc
前面的语句能否执行呢,答案是可以的。
此时的payload:')as/**/total/**/union/**/select/**/database();--
发现还是报错,但是如我们所料,报错的位置变了,前面的sql语句已经闭合,

前面已经提到了,永远都会报错,没法回显,那只能使用盲注试试到底能否执行sql语句,
payload:
1 |
|
然后发现响应延时了,说明这里确实会执行一条完整的语句,下面是mysql的日志:

这里埋坑,为什么报错了还会执行闭合的sql语句,分析在后续Mybatis底层源码调试部分。
Exp
时间盲注读数据库名脚本:
1 |
|
效果:

既然可以通过;
来闭合语句,自然想到堆叠注入,但是这里没法回显,只能create
、drop
等等操作,也是很危险的操作:
这里试着创建一个test
表:
')as/**/total/**/union/**/select/**/database();create/**/table/**/test(birthdate/**/DATE);--%20
虽然服务端报500错误,但是执行了

可以在数据库中看到这个新建的表:


后续利用这里就不说了,主要漏洞是SQL注入。
Mybatis底层源码调试
漏洞出现的表层原因是没有进行预编译就直接拼接用户输入,那为什么框架还会提示SQL语法错误却又奇迹般地执行了闭合的sql语句呢?
这需要调试底层源码了,该项目使用的是MP,不影响,底层仍然是Mybatis。
构造一个payload:')as/**/total/**/union/**/select/**/database();
,发包访问一下,服务端报错,通过报错的地方去下断点分析:
注意函数都是压栈,所以最下层的函数最先调用:

先在DefaultSqlSession.java
的151行位置打个断点,然后启动Debug,发包进入断点,这里会首先进行一次成功的SQL查询,直接放掉,从第二次报错的SQL查询开始调试,

快速步进,这里进入了一次Cache查询,当然缓存中是没有的,

之后进入到一次正常的查询:

随后就是对语句的预处理,进行QueryInfo
类的实例化
正常进入QueryInfo.getQueryReturnType()

QueryInfo.QueryInfo(String sql, Session session, String encoding)
:

正常执行到ClientPreparedStatement
实例化完成,没有任何报错,此时预处理已经完成

后续会调用SimpleExecutor.doQuery()
,进行SQL语句的执行:

后续进入代理预处理语句,

一直运行到NativeSession.execSQL
,到这里已经进入com.mysql.cj
包了,说明MyBatis
这个版本没有进行任何检查

进入NativeProtocol.sendQueryPacket
,其中有一个很重要的方法sendCommand
,当它执行过后就说明我们的SQL语句已经发送至数据库,等待数据库响应结果了,可以看到其后面就是在就算访问时间差和处理返回结果

分析到这里就完全清楚了,MyBatis
完全没有进行检查,当然这不是必须的,因为这是开发者的疏忽,采用#
进行预编译即可解决。
报错是因为SQL语句确实有错,%' order by createTime desc
就是一个错误语句,其前面的语句是正常执行了的,然后错误层层抛出,然后被Spring
框架捕捉,才会有另一层的函数调用,这都不重要了。
到这里,就解释清楚了,为什么会产生堆叠注入。由于无法回显,攻击手法也可以采用时间盲注。