虽然前面我已经写了一篇文章,“总结”了一些rmi的攻击类型,那篇文章我只是介绍了攻击方法,但是原理我不是很清楚,而且也不是太全。最近花了一些时间,调试了代码,算是大致搞清楚了rmi的具体流程,并写了一个工具 attackRmi 。这个工具使用socket模拟rmi协议直接发包,比直接调用java相关函数方便不少。为了搞懂rmi协议,还是花了一些力气。
本文会包含一下内容
- RMI 协议介绍
- RMI 攻击面
- attackRmi 实现
RMI 协议介绍
关于RMI已经有不少文章总结的比较全了,感谢各位的分享。比如
- https://xz.aliyun.com/t/7930
- https://xz.aliyun.com/t/7932
- https://blog.0kami.cn/2020/02/06/rmi-registry-security-problem/
他们基本都参考了
- https://mogwailabs.de/blog/2019/03/attacking-java-rmi-services-after-jep-290/
- https://mogwailabs.de/blog/2020/02/an-trinhs-rmi-registry-bypass/
然后这两篇文章基本上都是来自这篇blackhat
“客户端”:这里指的是主动发请求的
“服务端”:这里指的是接收处理请求的
下面贴了一些调用栈,方便大家自己下断点自己调试,要想搞清楚还得自己动手调试。
服务端处理请求主要包括三种Implement
RegistryImpl_Skel
- 主要是和registry相关的一些处理,主要是处理bind,unbind,rebind,list,lookup等请求
- 调用栈
1
2
3
4
5
6dispatch: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
6dispatch: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
9sayHello: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上的那篇,从“客户端” 发出的协议报文一般开头是这样的。
这些都是tcp的data部分,tcp那层省略了。
红色标记的部分是序列化数据。
关于具体的协议只找到了这个简略的文档。 https://docs.oracle.com/javase/9/docs/specs/rmi/protocol.html
- operation
- call
- 0x50
- ping
- 0x52
- DgcAck
- 0x54
- call
- 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必须设为一个负数,没有具体的含义
- bind
hash
- 在RegistryImpl_Skel情况下为interfaceHash是个固定值
- 4905912898345647071L
- 在DGCImpl_Skel情况下也是interfaceHash是个固定值
- -669196253586618813L
- 在自己写的Implement中为自己写的Class中方法签名的sha1
- 在RegistryImpl_Skel情况下为interfaceHash是个固定值
下面介绍一下如何计算自己实现方法的对应的hash。首先要了解java的方法签名。
参考这个 https://stackoverflow.com/questions/8066253/compute-a-java-functions-signature
1 | Signature Java Type |
比如下面sayHello这个method的签名就是sayHello(Ljava/lang/String;)Ljava/lang/String;
1 | public interface HelloInter extends Remote { |
格式就是methodName(params)return
然后从代码里扒拉出了通过上面的签名计算hash的代码
具体见 https://github.com/waderwu/attackRmi/blob/master/src/com/wu/attackRmi/utils/ComputeMethodHash.java 这个文件
1 | public static long computeMethodHash(String s) { |
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
- 0x01
uuid
- 还不清楚干啥的
- Object
- 就是返回的具体内容,可能是调用的返回值,也可能是Exception
对返回的解析也是在 https://github.com/waderwu/attackRmi/blob/master/src/com/wu/attackRmi/utils/Stub.java 只对lookup的情况进行了解析。
OK,到这里协议我们已经分析完了。
RMI 攻击面
- 8u121之前,可以通过bind,lookup,dgc等方式攻击Registry端口等直接反序列化
- 8u232之前,可以通过lookup发送一个UnicastRef对象,在反序列化的时候进行一次rmi链接,配合JRMPListener进行攻击。
- 8u242之前,可以通过lookup发送一个UnicastRefRemoteObject对象,在反序列化的时候进行一次rmi链接,配合JRMPListener进行攻击。
- 如果自己写的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.AnnotationInvocationHandler
的memberValues
。
如果直接发包,发送的时候直接在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.MarshalOutputStream
的resolveProxyClass
,遇到不存在的interface
换成用以MockInterface
为接口的动态代理类。
1 | protected Class<?> resolveProxyClass(String[] interfaces){ |
我能想到解决这个问题的方法有三个
- 自己解析那段bytes,从中提取出ip,port,objid
- 重写
resolveProxyClass
方法 - 加载前通过defineclass,把相应的interface.Class加载进来。
最后考虑到自己java水平不太行,我用了重写resolveProxyClass
这种方法,但是感觉第一种方法可能更好,有空实现一下。
attackRmi 使用方法
把代码clone下来
1
git clone https://github.com/waderwu/attackRmi.git
然后用idea打开,添加第三方库ysoserial
- 然后编辑相应的文件,更改参数就可以运行了。
欢迎大家报Bug或者PR,当然也欢迎Star!
其他
那篇blackhat提了其他攻击面,但是我没看太懂,它里面提到了通过控制num和http可以绕过rebind检查地址的限制,这个没看太懂。希望会的能教教我。
参考链接
- https://xz.aliyun.com/t/7930
- https://xz.aliyun.com/t/7932
- https://blog.0kami.cn/2020/02/06/rmi-registry-security-problem/
- https://mogwailabs.de/blog/2019/03/attacking-java-rmi-services-after-jep-290/
- https://mogwailabs.de/blog/2020/02/an-trinhs-rmi-registry-bypass/
- https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf