虽然前面我已经写了一篇文章,“总结”了一些rmi的攻击类型,那篇文章我只是介绍了攻击方法,但是原理我不是很清楚,而且也不是太全。最近花了一些时间,调试了代码,算是大致搞清楚了rmi的具体流程,并写了一个工具 attackRmi 。这个工具使用socket模拟rmi协议直接发包,比直接调用java相关函数方便不少。为了搞懂rmi协议,还是花了一些力气。
本文会包含一下内容
关于RMI已经有不少文章总结的比较全了,感谢各位的分享。比如
他们基本都参考了
然后这两篇文章基本上都是来自这篇blackhat
“客户端”:这里指的是主动发请求的
“服务端”:这里指的是接收处理请求的
下面贴了一些调用栈,方便大家自己下断点自己调试,要想搞清楚还得自己动手调试。
服务端处理请求主要包括三种Implement
RegistryImpl_Skel
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
调用栈
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
调用栈
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上的那篇,从“客户端” 发出的协议报文一般开头是这样的。
这些都是tcp的data部分,tcp那层省略了。
红色标记的部分是序列化数据。
关于具体的协议只找到了这个简略的文档。 https://docs.oracle.com/javase/9/docs/specs/rmi/protocol.html
operation
objid 是个ObjID的实例
对于RegistryImpl_Skel 和 DGCImpl_Skel 的Objid是固定的,对于自己写的Implementde Objid是随机生成的,这个需要事先通过lookup获取
new ObjID(0)
new ObjID(2)
num
在RegistryImpl_Skel中分别对应bind,list,lookup,rebind,unbind这5种操作
在DGCImpl_Skel中分别对应clean和dirty这两种操作
在自己写的implement中,num必须设为一个负数,没有具体的含义
下面介绍一下如何计算自己实现方法的对应的hash。首先要了解java的方法签名。
参考这个 https://stackoverflow.com/questions/8066253/compute-a-java-functions-signature
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;
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 这个文件
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
returnType
有两种一种是normal的return一种是exception的return
(TransportConstants.NormalReturn
TransportConstants.ExceptionalReturn
uuid
Object
对返回的解析也是在 https://github.com/waderwu/attackRmi/blob/master/src/com/wu/attackRmi/utils/Stub.java 只对lookup的情况进行了解析。
OK,到这里协议我们已经分析完了。
8u121之前,可以通过bind,lookup,dgc等方式攻击Registry端口等直接反序列化
8u232之前,可以通过lookup发送一个UnicastRef对象,在反序列化的时候进行一次rmi链接,配合JRMPListener进行攻击。
8u242之前,可以通过lookup发送一个UnicastRefRemoteObject对象,在反序列化的时候进行一次rmi链接,配合JRMPListener进行攻击。
如果自己写的implement中method包含非primitive类型的参数(8u242之后string也不行),也能进行反序列化攻击。
限制
下面贴了attackRmi的几种攻击方法,具体的原理请阅读参考链接,openjdk链接是相对应版本改进的代码。
条件:
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就可以了,不需要再用动态代理转成相应的类型。下面几个实现都是直接发包的。
条件:
https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/75f31e0bd829/
条件:
https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/523d48606333/
条件:
https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/033462472c28
条件:
https://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/033462472c28
刚开始是想用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
为接口的动态代理类。
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; }
我能想到解决这个问题的方法有三个
resolveProxyClass
方法最后考虑到自己java水平不太行,我用了重写resolveProxyClass
这种方法,但是感觉第一种方法可能更好,有空实现一下。
把代码clone下来
git clone https://github.com/waderwu/attackRmi.git
然后用idea打开,添加第三方库ysoserial
然后编辑相应的文件,更改参数就可以运行了。
欢迎大家报Bug或者PR,当然也欢迎Star!
那篇blackhat提了其他攻击面,但是我没看太懂,它里面提到了通过控制num和http可以绕过rebind检查地址的限制,这个没看太懂。希望会的能教教我。