高版本Fastjson在Java原生反序列化中的利用
2025-1-11 14:31:0 Author: mp.weixin.qq.com(查看原文) 阅读量:1 收藏

目录

  • • 前言
  • • 绕过思路一:从已知gadget中寻找TemplatesImpl替代品
    • • ReferenceSerialized
    • • LdapAttribute
  • • 绕过思路二:利用JDBC-Attack替换TemplatesImpl
    • • mchange-commons-java
    • • c3p0
    • • postgresql
    • • mysql
  • • Fastjson遍历getter方法的顺序
  • • 绕过思路三:使用动态代理
    • • JdkDynamicAopProxy
    • • ObjectFactoryDelegatingInvocationHandler+JSONObject
  • • 代码示例

前言

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

绕过思路一:从已知gadget中寻找TemplatesImpl替代品

第一种绕过的思路是寻找TemplatesImpl的替代品,既然JsonArray可以调用任意公开getter方法,那么只要寻找到一个在黑名单外且通过getter方法触发利用的类即可,先从已有gadget chain中物色一下替代者

ReferenceSerialized

首先想到的是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:-1603650290 (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如下:


注:实现代码见最后一节代码示例

LdapAttribute

com.sun.jndi.ldap.LdapAttribute也是一个可以替代TemplatesImpl的类,jdk自带,LdapAttribute在2021 年realworldctf 中由voidfyoo 发现。LdapAttribute#getAttributeDefinition方法可以触发一次JNDI注入。

gadget chain如下:


绕过思路二:利用JDBC-Attack替换TemplatesImpl

ReferenceSerializedLdapAttribute都是已知的gadget,但个人感觉这两个类在实际利用中都有一些缺陷,ReferenceSerialized的依赖并非大热门,LdapAttribute转JNDI注入的利用方式也不太友好,高版本jdk的JNDI利用一般是通过反序列化或者找本地的ObjectFactory,搞不好兜兜转转又回到了反序列化。

于是就想着寻找一个新的通过getter方法利用的gadget,正好这几年JDBC-Attack比较热门,部分数据库JDBC-Attack利用方式不依赖反序列化或JNDI,可以直接执行代码或读取文件(例如H2、Pgsql、Mysql),因此就往这个方向靠了一下。

  1. 1. JDBC-Attack的常见入口方法为java.sql.DriverManager#getConnection,可以用这个静态方法作为污点往前找。
  2. 2. JDBC-Attack的实际触发点是在各个数据库java.sql.Driver实现类的connect方法,该方法也是污点之一(实际上包含前者)。不过Driver接口本身并没有继承Serializable接口,因此还依赖方法自行动态创建/获取Driver实现类。

mchange-commons-java

涉及类: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依赖来利用。

c3p0

涉及类:com.mchange.v2.c3p0.DriverManagerDataSourcecom.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>

postgresql

以上的两个依赖中的gadget都来自数据库连接池,本身只有触发JDBC连接的能力,实际利用还需要结合对应的数据库JDBC依赖。下面直接从数据库JDBC依赖本身寻找完整的调用链。

涉及类:org.postgresql.ds.PGSimpleDataSourceorg.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>

mysql

涉及类: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方法的顺序

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方法可以分为如下几个阶段:

  1. 1. 通过反射objectClass.getMethods(),获取所有公开getter方法,将方法转换成FieldWriter
  2. 2. 将FieldWriter按上面的获取顺序依次放入LinkedHashMap,然后将LinkedHashMap的值全部放入一个ArrayList,记为fieldWriters
  3. 3. 通过对Collections.sort(fieldWriters)进行排序
  4. 4. 根据排序的结果写入字节码,这也是最终的调用顺序。

具体代码在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

对于其他常用的数据库连接池,例如:dbcpdruid,要转为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,在JsonArrayTemplatesImpl之间加入一个“中间节点”也是一种思路。进一步分析IGNORE_CLASS_HASH_CODES黑名单列表,会发现TemplatesImpl的上层接口javax.xml.transform.Templates并不在其中,而getOutputProperties方法正是Templates接口定义的方法,于是很容易就想到通过动态代理来充当这个“中间节点”。

JdkDynamicAopProxy

org.springframework.aop.framework.JdkDynamicAopProxy来源于spring-aop依赖,这个gadget在JSON1和Spring2两条gadget chain均有使用。

这里需要先提一下笔者之前对JSON1的改造过程,在分析JSON1这条gadget chain时,注意到它使用了三个动态代理:

  • • CompositeInvocationHandlerImpl:jdk自带,能根据不同的方法所属的类,选择不同的InvocationHandler,可以看作InvocationHandler的代理。
  • • AnnotationInvocationHandler:同样jdk自带,也是反序列化gdaget的常客了,它在JSON1中承担的责任实际类似一个"垃圾桶",当调用CompositeData#getCompositeType()方法时可以返回一个CompositeType实例,避免报错抛异常。
  • • JdkDynamicAopProxy:来自spring-aop依赖,反射调用方法。
JSON1使用三个动态代理的原因是JSONObject获取getter方法不固定,有可能会先调用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如下:


ObjectFactoryDelegatingInvocationHandler+JSONObject

除了JdkDynamicAopProxy,是否有更通用的动态代理呢?抱着来都来了的想法,把jdk内置和常用依赖中同时实现了InvocationHandlerSerializable接口的类都看了一遍(实际上并不多,只有几十个),注意到了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方法返回一个指定的对象。

最终方案:

  1. 1. 用ObjectFactoryDelegatingInvocationHandler代理Templates接口,被调用getOutputProperties方法
  2. 2. 用JSONObject代理ObjectFactoryDelegatingInvocationHandler中的objectFactory属性,返回teamplatesImpl

调用链如下:


代码示例

https://github.com/Ape1ron/FastjsonInDeserializationDemo1


文章来源: https://mp.weixin.qq.com/s?__biz=Mzg2MDY2ODc5MA==&mid=2247484185&idx=1&sn=9068c43597d87c94568fe70974fd6365&chksm=ce239500f9541c160287b545120d6495c7a2aa9c5c75e0ad101c7a3d3600e86ea6b64ef75f63&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh