0%

attack-rmi-registry-and-server-with-socket

虽然前面我已经写了一篇文章,“总结”了一些rmi的攻击类型,那篇文章我只是介绍了攻击方法,但是原理我不是很清楚,而且也不是太全。最近花了一些时间,调试了代码,算是大致搞清楚了rmi的具体流程,并写了一个工具 attackRmi 。这个工具使用socket模拟rmi协议直接发包,比直接调用java相关函数方便不少。为了搞懂rmi协议,还是花了一些力气。

本文会包含一下内容

  • RMI 协议介绍
  • RMI 攻击面
  • attackRmi 实现

RMI 协议介绍

关于RMI已经有不少文章总结的比较全了,感谢各位的分享。比如

他们基本都参考了

然后这两篇文章基本上都是来自这篇blackhat

“客户端”:这里指的是主动发请求的

“服务端”:这里指的是接收处理请求的

下面贴了一些调用栈,方便大家自己下断点自己调试,要想搞清楚还得自己动手调试。

服务端处理请求主要包括三种Implement

  • RegistryImpl_Skel

    • 主要是和registry相关的一些处理,主要是处理bind,unbind,rebind,list,lookup等请求
    • 调用栈
    1
    2
    3
    4
    5
    6
    dispatch:129, RegistryImpl_Skel (sun.rmi.registry)
    oldDispatch:469, UnicastServerRef (sun.rmi.server)
    dispatch:301, UnicastServerRef (sun.rmi.server)
    ......
    serviceCall:196, Transport (sun.rmi.transport)
    handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
  • DGCImpl_Skel

    • 主要处理DGC请求

    调用栈

    1
    2
    3
    4
    5
    6
    dispatch:88, DGCImpl_Skel (sun.rmi.transport)
    oldDispatch:469, UnicastServerRef (sun.rmi.server)
    dispatch:301, UnicastServerRef (sun.rmi.server)
    ......
    serviceCall:196, Transport (sun.rmi.transport)
    handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
  • 还有一类自己写的Implement

    • 处理自定义方法的调用

    调用栈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    sayHello:8, HelloImpl (com.wu)
    invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
    invoke:62, NativeMethodAccessorImpl (sun.reflect)
    invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
    invoke:498, Method (java.lang.reflect)
    dispatch:357, UnicastServerRef (sun.rmi.server)
    ......
    serviceCall:196, Transport (sun.rmi.transport)
    handleMessages:573, TCPTransport (sun.rmi.transport.tcp)

下面我从网络数据包的角度分析一下RMI协议。

按照blackhat上的那篇,从“客户端” 发出的协议报文一般开头是这样的。

image-20200901193909144

这些都是tcp的data部分,tcp那层省略了。

红色标记的部分是序列化数据。

关于具体的协议只找到了这个简略的文档。 https://docs.oracle.com/javase/9/docs/specs/rmi/protocol.html

  • operation
    • call
      • 0x50
    • ping
      • 0x52
    • DgcAck
      • 0x54
  • objid 是个ObjID的实例

对于RegistryImpl_Skel 和 DGCImpl_Skel 的Objid是固定的,对于自己写的Implementde Objid是随机生成的,这个需要事先通过lookup获取

  • RegistryImpl_Skel
    • new ObjID(0)
  • DGCImpl_Skel
    • new ObjID(2)
  • num

    在RegistryImpl_Skel中分别对应bind,list,lookup,rebind,unbind这5种操作

    • bind
      • 0
    • list
      • 1
    • lookup
      • 2
    • rebind
      • 3
    • unbind
      • 4

    在DGCImpl_Skel中分别对应clean和dirty这两种操作

    • clean
      • 0
    • dirty
      • 1

    在自己写的implement中,num必须设为一个负数,没有具体的含义

  • hash

    • 在RegistryImpl_Skel情况下为interfaceHash是个固定值
      • 4905912898345647071L
    • 在DGCImpl_Skel情况下也是interfaceHash是个固定值
      • -669196253586618813L
    • 在自己写的Implement中为自己写的Class中方法签名的sha1

下面介绍一下如何计算自己实现方法的对应的hash。首先要了解java的方法签名。

参考这个 https://stackoverflow.com/questions/8066253/compute-a-java-functions-signature

1
2
3
4
5
6
7
8
9
10
11
12
Signature    Java Type
Z boolean
B byte
C char
S short
I int
J long
F float
D double
V void
L fully-qualified-class ; fully-qualified-class
[ type type[]

比如下面sayHello这个method的签名就是sayHello(Ljava/lang/String;)Ljava/lang/String;

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

格式就是methodName(params)return

然后从代码里扒拉出了通过上面的签名计算hash的代码

具体见 https://github.com/waderwu/attackRmi/blob/master/src/com/wu/attackRmi/utils/ComputeMethodHash.java 这个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static long computeMethodHash(String s) {
long hash = 0;
ByteArrayOutputStream sink = new ByteArrayOutputStream(127);
try {
MessageDigest md = MessageDigest.getInstance("SHA");
DataOutputStream out = new DataOutputStream(new DigestOutputStream(sink, md));
out.writeUTF(s);

// use only the first 64 bits of the digest for the hash
out.flush();
byte hasharray[] = md.digest();
for (int i = 0; i < Math.min(8, hasharray.length); i++) {
hash += ((long) (hasharray[i] & 0xFF)) << (i * 8);
}
} catch (IOException ignore) {
/* can't happen, but be deterministic anyway. */
hash = -1;
} catch (NoSuchAlgorithmException complain) {
throw new SecurityException(complain.getMessage());
}
return hash;
}
  • Object

    对应不同的场景会不一样,基本上都是一些参数序列化之后的结果

    • 对bind 来说就是String和remote参数
    • 对lookup来说就是String类型的name
    • 对dirty来说就是ObjID,Lease等类型参数
    • 对于自己写的implemnt就是调用时传的参数

反序列化漏洞基本都是发生在反序列化这些参数的时候(还有部分是主动发起rmi请求,反序列返回值的时候出的问题),后面的一些安全措施也是在这上面做的手脚,比如lookup参数只是String类型,在8u242之后就只序列化String类型的,这样就把lookup这条攻击链给断了。dirty的参数类型也是固定的,在jep290的时候就被限制了。但是用户自己写的方法参数类型可能多种多样,不方便限制,所以基本到现在最新的JDK就只剩这一条路了,这条路在8u242也对String类型参数进行了特殊处理。

上面对发送的报文类型介绍基本上差不多了。

所以可以根据上面的介绍,自己写个socket直接发送上面的数据。

这个实现在 https://github.com/waderwu/attackRmi/blob/master/src/com/wu/attackRmi/utils/Stub.java

主要参考了 https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/JRMPClient.java 的实现

下面开始介绍返回报文。

红色框出来的也是序列化的内容

  • returnVale
    • 0x51
  • returnType

    有两种一种是normal的return一种是exception的return

    • 0x01
      • (TransportConstants.NormalReturn
    • 0x02
      • TransportConstants.ExceptionalReturn
  • uuid

    • 还不清楚干啥的
  • Object
    • 就是返回的具体内容,可能是调用的返回值,也可能是Exception

对返回的解析也是在 https://github.com/waderwu/attackRmi/blob/master/src/com/wu/attackRmi/utils/Stub.java 只对lookup的情况进行了解析。

OK,到这里协议我们已经分析完了。

RMI 攻击面

  1. 8u121之前,可以通过bind,lookup,dgc等方式攻击Registry端口等直接反序列化
  2. 8u232之前,可以通过lookup发送一个UnicastRef对象,在反序列化的时候进行一次rmi链接,配合JRMPListener进行攻击。
  3. 8u242之前,可以通过lookup发送一个UnicastRefRemoteObject对象,在反序列化的时候进行一次rmi链接,配合JRMPListener进行攻击。
  4. 如果自己写的implement中method包含非primitive类型的参数(8u242之后string也不行),也能进行反序列化攻击。

限制

  • 1,2,3,4都需要本地包含gadgets
  • 2,3 需要能出网
  • 1,2,3都可以直接攻击Registry端口(1099), 4还需要额外的端口
  • 4 需要知道具体的方法,所以还需要有源码,还要能访问非1099端口

下面贴了attackRmi的几种攻击方法,具体的原理请阅读参考链接,openjdk链接是相对应版本改进的代码。

AttackRegistryByBindAndAnnotationInvocationHandler

条件:

  • < jdk8u121

https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/75f31e0bd829/

由于bind(String, Remote) 第一个参数必须是string,第二个必须是Remote,不能直接把conmoncollections的payload放进去。

ysoserial中RMIRegistryExploit是通过动态代理,把payload塞到sun.reflect.annotation.AnnotationInvocationHandlermemberValues

如果直接发包,发送的时候直接在Object那个位置,贴上我们序列化的payload就可以了,不需要再用动态代理转成相应的类型。下面几个实现都是直接发包的。

AttackRegistryByDGC

条件:

  • < jdk8u121

https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/75f31e0bd829/

AttackRegistryByLookupAndUnicastRef

条件:

  • < jdk8u232

https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/523d48606333/

AttackRegistryByLookupAndUnicastRefRemoteObject

条件:

  • < jdk8u242

https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/033462472c28

AttackServerByNonPrimitiveParameter

条件:

  • < jdk8u242
    • 除primitive tyep以外的类型可被利用
  • >= jdk8u242
    • 除primitive type和String以外的类型可被利用

https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/033462472c28

attackRmi 实现

刚开始是想用python socket直接发包,因为原先用python socket写过东西,交互写起来更顺手,但是拼接序列化内容的时候出问题了。我原先直接用的是ObjectOutputStream 进行的序列化,但是rmi中用的是sun.server.rmi.MarshalOutputStream

后来意外发现了ysoserial的JRMPclient的实现,然后就在开始用java的socket写,在makeDgcCall的基础上改进。刚开始用jdk自带的sun.server.rmi.MarshalOutputStream 没有问题,但是传UnicastRefRemoteObject 对象的时候,发现死活传不过去,后来发现jdk自带的sun.server.rmi.marshalOutputStream 会进行replaceObject,后来就直接换成了ysoserial中的MarshalOutputStream 这样就没啥问题了。

在实现AttackServerByNonPrimitiveParameter 遇到了其他问题,比如刚开始不知道咋获取objid,后来跟代码的时候发现在lookup返回的对象里面,然后通过反射将其值读出来。但是要是想用lookup的时候,本地必须要先有个interface,要不然lookup在收到返回数据反序列化的时候会报classnotfound,这里我重写了sun.server.rmi.MarshalOutputStreamresolveProxyClass ,遇到不存在的interface换成用以MockInterface为接口的动态代理类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected Class<?> resolveProxyClass(String[] interfaces){
Class clazz;
try{
clazz = Class.forName(interfaces[0]);
}catch (ClassNotFoundException e){
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 2333);
UnicastRef refObject = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler myInvocationHandler = new RemoteObjectInvocationHandler(refObject);

MockInterface proxy = (MockInterface) Proxy.newProxyInstance(MockInterface.class.getClassLoader(), new Class[] { MockInterface.class, Remote.class }, myInvocationHandler);
clazz = proxy.getClass();
return clazz;
}
try {
return super.resolveProxyClass(interfaces);
}catch (Exception ee){
ee.printStackTrace();
}
return clazz;
}

我能想到解决这个问题的方法有三个

  • 自己解析那段bytes,从中提取出ip,port,objid
  • 重写resolveProxyClass方法
  • 加载前通过defineclass,把相应的interface.Class加载进来。

最后考虑到自己java水平不太行,我用了重写resolveProxyClass这种方法,但是感觉第一种方法可能更好,有空实现一下。

attackRmi 使用方法

  1. 把代码clone下来

    1
    git clone https://github.com/waderwu/attackRmi.git
  2. 然后用idea打开,添加第三方库ysoserial

  3. 然后编辑相应的文件,更改参数就可以运行了。

欢迎大家报Bug或者PR,当然也欢迎Star!

其他

那篇blackhat提了其他攻击面,但是我没看太懂,它里面提到了通过控制num和http可以绕过rebind检查地址的限制,这个没看太懂。希望会的能教教我。

参考链接