2023年3月,@y4tacker在博客公开了一条仅依赖fastjson的原生反序列化gadget chain,影响当时fastjson的所有版本(到2.0.26),博客原文:https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/。
整条gadget chain如下,核心是Fastjson中的JsonArray
类,该类被调用toString
方法时,可遍历调用其元素的任意公开getter方法,从而触发TemplatesImpl#getOutputProperties
方法,加载字节码完成代码执行。
注: HashMap
的作用是为了保持一个TemplatesImpl
的反序列化引用,绕过SecureObjectInputStream
重写resolveClass
的限制。
2023年4月,Fastjson更新了2.0.27版本,在com.alibaba.fastjson2.util.BeanUtils
中增加了黑名单限制,在黑名单中的类不会被调用getter方法,TemplatesImpl
也被加入了黑名单,导致该gadget chain无法直接利用。
JsonArray
在调用其元素getter方法时,有一个通过ASM生成字节码的过程,对比2.0.26与2.0.27版本生成的最终代码,可以看到TemplatesImpl#getOutputProperties
方法不再被调用。
到目前最新的2.0.53版本,一共有24个黑名单,在前期黑名单是明文的类名,后面变成了根据类名计算出一个hashCode64,代码在com.alibaba.fastjson2.util.Fnv#hashCode64
方法,魔改一下fastjson-blacklist项目(替换hashCode计算函数)找出所有黑名单类列表如下:
-9214723784238596577L, // javassist.CtMethod
-9030616758866828325L, // org.apache.xalan.xsltc.trax.TemplatesImpl
-8335274122997354104L, // org.apache.ibatis.javassist.CtNewClass
-6963030519018899258L, // org.apache.ibatis.javassist.CtClass
-4863137578837233966L, // javassist.CtClass
-3653547262287832698L, // org.apache.ibatis.javassist.CtConstructor
-2819277587813726773L, // org.apache.ibatis.javassist.CtMethod
-2669552864532011468L, // java.lang.ref.ReferenceQueue
-2458634727370886912L, // java.security.ProtectionDomain
-2291619803571459675L, // com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl
-1811306045128064037L, // org.apache.xalan.xsltc.trax.TransformerFactoryImpl
-864440709753525476L, // org.apache.xalan.xsltc.runtime.AbstractTranslet
-779604756358333743L, // org.mockito.internal.creation.bytebuddy.MockMethodInterceptor
8731803887940231L, // org.apache.commons.collections.functors.ChainedTransformer
1616814008855344660L, // javassist.CtNewClass
2164749833121980361L, // com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
2688642392827789427L, // java.util.concurrent.locks.Lock
3724195282986200606L, // org.apache.wicket.util.io.DeferredFileOutputStream
3742915795806478647L, // java.io.InputStream
3977020351318456359L, // sun.nio.ch.FileChannelImpl
4882459834864833642L, // javassist.CtConstructor
6033839080488254886L, // java.util.concurrent.locks.ReentrantLock
7981148566008458638L, // com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
8344106065386396833L // javassist.CtNewNestedClass
第一种绕过的思路是寻找TemplatesImpl
的替代品,既然JsonArray
可以调用任意公开getter方法,那么只要寻找到一个在黑名单外且通过getter方法触发利用的类即可,先从已有gadget chain中物色一下替代者。
首先想到的是com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized
,该类在C3P0 gadget chain中首次出现,所需依赖:com.mchange:mchange-commons-java
。
ReferenceSerialized#getObject
方法可以通过URLClassLoader
进行一次远程类加载,调用栈如下:
referenceToObject:91, ReferenceableUtils (com.mchange.v2.naming)
getObject:118, ReferenceIndirector$ReferenceSerialized (com.mchange.v2.naming)
apply:-1, 603650290 (com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized$$Lambda$23)
getFieldValue:40, FieldWriterObjectFunc (com.alibaba.fastjson2.writer)
write:256, FieldWriterObject (com.alibaba.fastjson2.writer)
write:68, ObjectWriter1 (com.alibaba.fastjson2.writer)
write:364, ObjectWriterImplList (com.alibaba.fastjson2.writer)
toJSONString:1647, JSON (com.alibaba.fastjson)
toString:904, JSONArray (com.alibaba.fastjson)
readObject:86, BadAttributeValueExpException (javax.management)
...
修改后的gadget chain如下:
注:实现代码见最后一节代码示例
com.sun.jndi.ldap.LdapAttribute
也是一个可以替代TemplatesImpl
的类,jdk自带,LdapAttribute
在2021 年realworldctf 中由voidfyoo 发现。LdapAttribute#getAttributeDefinition
方法可以触发一次JNDI注入。
gadget chain如下:
ReferenceSerialized
和LdapAttribute
都是已知的gadget,但个人感觉这两个类在实际利用中都有一些缺陷,ReferenceSerialized
的依赖并非大热门,LdapAttribute
转JNDI注入的利用方式也不太友好,高版本jdk的JNDI利用一般是通过反序列化或者找本地的ObjectFactory,搞不好兜兜转转又回到了反序列化。
于是就想着寻找一个新的通过getter方法利用的gadget,正好这几年JDBC-Attack比较热门,部分数据库JDBC-Attack利用方式不依赖反序列化或JNDI,可以直接执行代码或读取文件(例如H2、Pgsql、Mysql),因此就往这个方向靠了一下。
java.sql.DriverManager#getConnection
,可以用这个静态方法作为污点往前找。java.sql.Driver
实现类的connect
方法,该方法也是污点之一(实际上包含前者)。不过Driver接口本身并没有继承Serializable
接口,因此还依赖方法自行动态创建/获取Driver
实现类。涉及类:com.mchange.v1.db.sql.DriverManagerDataSource
调用链:
com.mchange.v1.db.sql.DriverManagerDataSource#getConnection()->
java.sql.DriverManager#getConnection(java.lang.String, java.util.Properties)
<dependency>
<groupId>com.mchange</groupId>
<artifactId>mchange-commons-java</artifactId>
<version>0.2.19</version>
</dependency>
DriverManagerDataSource
只能作为JDBC-Attack的入口,因为依赖本身不包含JDBC-Attack利用的,还需要结合数据库的JDBC依赖来利用。
涉及类:com.mchange.v2.c3p0.DriverManagerDataSource
、com.mchange.v2.c3p0.test.FreezableDriverManagerDataSource
,这两个类都是com.mchange.v2.c3p0.impl.DriverManagerDataSourceBase
的子类。
调用链:
# 1
com.mchange.v2.c3p0.DriverManagerDataSource#getConnection()->
java.sql.Driver#connect()
# 2
com.mchange.v2.c3p0.test.FreezableDriverManagerDataSource#getConnection()->
java.sql.Driver#connect
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.5</version>
</dependency>
以上的两个依赖中的gadget都来自数据库连接池,本身只有触发JDBC连接的能力,实际利用还需要结合对应的数据库JDBC依赖。下面直接从数据库JDBC依赖本身寻找完整的调用链。
涉及类:org.postgresql.ds.PGSimpleDataSource
、org.postgresql.ds.PGConnectionPoolDataSource
。
两个类都是org.postgresql.ds.common.BaseDataSource
的子类,真正起作用的也是BaseDataSource#getConnection
方法,但BaseDataSource
本身是抽象类,也没有实现Serializable
接口,这两个子类恰好补足了利用条件。
调用链:
org.postgresql.ds.common.BaseDataSource#getConnection()->
org.postgresql.ds.common.BaseDataSource#getConnection(java.lang.String, java.lang.String)->
java.sql.DriverManager#getConnection(java.lang.String, java.lang.String, java.lang.String)
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.1</version>
</dependency>
涉及类:com.mysql.jdbc.jdbc2.optional.MysqlDataSource
调用链:
com.mysql.jdbc.jdbc2.optional.MysqlDataSource#getConnection()->
com.mysql.jdbc.NonRegisteringDriver#connect
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
Fastjson遍历调用getter方法是有固定顺序的,其实一开始并没有注意到这个问题,以为和JSON1一样,通过反射获取方法列表的顺序并不固定。
注意到这个问题是因为一个失败的例子:org.apache.tomcat.dbcp.dbcp2.cpdsadapter.DriverAdapterCPDS#DriverAdapterCPDS
。
和c3p0
类似,dbcp
也是常用的数据库连接池,一开始也纳入了搜索范围,并且注意到有如下调用链:
org.apache.tomcat.dbcp.dbcp2.cpdsadapter.DriverAdapterCPDS#getPooledConnection() ->
org.apache.tomcat.dbcp.dbcp2.cpdsadapter.DriverAdapterCPDS#getPooledConnection(java.lang.String, java.lang.String)->
java.sql.DriverManager#getConnection(java.lang.String, java.util.Properties)
利用时发现报错了,原因出在DriverAdapterCPDS#getParentLogger
方法,直接抛出了异常。
这个方法继承自javax.sql.CommonDataSource
接口,从jdk1.7开始增加,官方注释中直接说明了,如果不需要的话可以直接抛出异常:
回头看上面成功利用的几个gadget,会发现大部分都实现了javax.sql.DataSource
接口,这是由于该接口定义了getConnection方法,刚好同时满足getter方法和建立JDBC连接(只不过需要子类将getConnection修改为public,并实现Serializable接口)。
但仔细一看,会发现DataSource
本身就继承了CommonDataSource
,这意味着上面的可用类本身也应该实现getParentLogger
方法,经过排查发现的确如此,并且大部分方法代码中都直接抛出异常,例如PGSimpleDataSource
但在多次测试后(包括更换jdk版本和依赖版本),发现利用都很稳定,PGSimpleDataSource
稳定利用,DriverAdapterCPDS
稳定报错,因此怀疑fastjson调用getter方法并不是随机的。
通过调试分析,fastjson调用getter方法可以分为如下几个阶段:
objectClass.getMethods()
,获取所有公开getter方法,将方法转换成FieldWriter
FieldWriter
按上面的获取顺序依次放入LinkedHashMap
,然后将LinkedHashMap
的值全部放入一个ArrayList
,记为fieldWriters
Collections.sort(fieldWriters)
进行排序具体代码在com.alibaba.fastjson2.writer.ObjectWriterCreatorASM#createObjectWriter
结果稳定正是因为使用了Collections.sort(fieldWriters)
进行排序,FieldWriter
实现了Comparable
接口,比较方法如下:ordinal
默认为0(通过注解设置),fieldName
属性实际上是getter方法的对应的属性名,例如getAa的属性名是aa,isBb的属性名是bb。
这就是PGSimpleDataSource
成功,而DriverAdapterCPDS
失败的原因,因为调用getter方法的顺序是固定的:
connection < parentLogger < pooledConnection
对于其他常用的数据库连接池,例如:dbcp
、druid
,要转为JDBC-Attack利用,可以先将反序列化转为JNDI(由于jdk原生自带的LdapAttribute
等类,要转JNDI是比较容易的),然后通过JNDI本地ObjectFactory的利用方式来实现。
JNDI本地ObjectFactory转JDBC-Attack,可以参考@浅蓝的议题:https://github.com/iSafeBlue/presentation-slides/blob/main/BCS2022-%E6%8E%A2%E7%B4%A2JNDI%E6%94%BB%E5%87%BB.pdf
要找到一个好用的TemplatesImpl
替代品并不容易,要么是所需依赖不太常见或对版本要求较高,要么是利用方式不太友好。
因此就尝试了转换一个思路,除了替换TemplatesImpl
,在JsonArray
和TemplatesImpl
之间加入一个“中间节点”也是一种思路。进一步分析IGNORE_CLASS_HASH_CODES
黑名单列表,会发现TemplatesImpl
的上层接口javax.xml.transform.Templates
并不在其中,而getOutputProperties
方法正是Templates
接口定义的方法,于是很容易就想到通过动态代理来充当这个“中间节点”。
org.springframework.aop.framework.JdkDynamicAopProxy
来源于spring-aop依赖,这个gadget在JSON1和Spring2两条gadget chain均有使用。
这里需要先提一下笔者之前对JSON1的改造过程,在分析JSON1这条gadget chain时,注意到它使用了三个动态代理:
InvocationHandler
,可以看作InvocationHandler
的代理。CompositeData#getCompositeType()
方法时可以返回一个CompositeType
实例,避免报错抛异常。但在jdk8u71-b12
之后,AnnotationInvocationHandler
代码进行了修改,无法再代理非注解的接口方法,这就导致了AnnotationInvocationHandler
无法再代理CompositeData
接口,此时再调用getCompositeType
方法就会报错。
要在高版本中稳定利用,需要替换掉AnnotationInvocationHandler
。注意到JdkDynamicAopProxy
的能力,容易想到再新建一个JdkDynamicAopProxy
实例来代理CompositeData
接口的子类(同时要求实现Serializable
接口),并找到了一个jdk自带的CompositeData
实现类:javax.management.openmbean.CompositeDataSupport
。
改造后的gadget chain如下,可以在不新增依赖的情况下在高版本jdk实现稳定利用。
于是在尝试通过动态代理绕过高版本fastjson限制时,几乎马上就想起JdkDynamicAopProxy
,并最终确认可用的。改造后的gadget chain如下:
除了JdkDynamicAopProxy
,是否有更通用的动态代理呢?抱着来都来了的想法,把jdk内置和常用依赖中同时实现了InvocationHandler
和Serializable
接口的类都看了一遍(实际上并不多,只有几十个),注意到了ObjectFactoryDelegatingInvocationHandler
这个类,来自spirng-beans(实际上这个类首次出现是在Spring1 gadget chain中,配合AnnotationInvocationHandler
来使用)。
观察ObjectFactoryDelegatingInvocationHandler
的代码,也有反射调用方法的能力,不同之处在于反射对象是通过objectFactory.getObject()
提供的。
我们的目标是让this.objectFactory.getObject()
返回teamplatesImpl
,让一个方法返回一个指定的对象,怎么看起来如此熟悉,这不就是前面提到的AnnotationInvocationHandler
的作用之一吗?
但也正如前面分析,高版本jdk下的AnnotationInvocationHandler
已经无法代理非注解类了,先简单看了一下org.springframework.beans.factory.ObjectFactory
的子类,没有找到直接满足目标的。所以思路又回到了动态代理上面,能不能找到一个类似AnnotationInvocationHandler
作用的动态代理,也即:通过动态代理调用,让特定方法返回一个指定的对象。
最终发现Fatsjson自身的com.alibaba.fastjson2.JSONObject
就是一个满足需求的动态代理:可以让任意getter方法返回一个指定的对象。
最终方案:
ObjectFactoryDelegatingInvocationHandler
代理Templates
接口,被调用getOutputProperties
方法JSONObject
代理ObjectFactoryDelegatingInvocationHandler
中的objectFactory
属性,返回teamplatesImpl
调用链如下:
https://github.com/Ape1ron/FastjsonInDeserializationDemo1