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方法,将方法转换成FieldWriterFieldWriter按上面的获取顺序依次放入LinkedHashMap,然后将LinkedHashMap的值全部放入一个ArrayList,记为fieldWritersCollections.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实例,避免报错抛异常。getCompositeType,然后再调用getOutputProperties。为了解决这个不稳定性,JSON1作者选择了使用三个动态代理,AnnotationInvocationHandler的其中一个能力就是可以让代理方法返回一个特定的对象,这样即便先调用getCompositeType方法也不会报错。但在jdk8u71-b12之后,AnnotationInvocationHandler代码进行了修改,无法再代理非注解的接口方法,这就导致了AnnotationInvocationHandler无法再代理CompositeData接口,此时再调用getCompositeType方法就会报错。
要在高版本中稳定利用,需要替换掉AnnotationInvocationHandler。注意到JdkDynamicAopProxy的能力,容易想到再新建一个JdkDynamicAopProxy实例来代理CompositeData接口的子类(同时要求实现Serializable接口),并找到了一个jdk自带的CompositeData实现类:javax.management.openmbean.CompositeDataSupport。
改造后的gadget chain如下,可以在不新增依赖的情况下在高版本jdk实现稳定利用。
JdkDynamicAopProxy动态代理留下了很深的印象。于是在尝试通过动态代理绕过高版本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