开篇
这篇文章简单的分析 Log4j2的临时解决方案的思路,同时分析下 jndi攻击的源码代码。临时性解决方案的核心思路是:log4j2.formatMsgNoLookups设置为True。通过添加 -Dlog4j2.formatMsgNoLookups=true或创建 “log4j2.component.properties” 文件并增加配置 “log4j2.formatMsgNoLookups=true”。
- 参考JndiLookup官网链接
调用栈
2021-12-12 20:13:12,359 main WARN Error looking up JNDI resource [ldap://192.168.0.3:1389/BugFinder]. javax.naming.CommunicationException: 192.168.0.3:1389 [Root exception is java.net.ConnectException: Connection refused (Connection refused)]
at com.sun.jndi.ldap.Connection.<init>(Connection.java:238)
at com.sun.jndi.ldap.LdapClient.<init>(LdapClient.java:137)
at com.sun.jndi.ldap.LdapClient.getInstance(LdapClient.java:1615)
at com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2749)
at com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:319)
at com.sun.jndi.url.ldap.ldapURLContextFactory.getUsingURLIgnoreRootDN(ldapURLContextFactory.java:60)
at com.sun.jndi.url.ldap.ldapURLContext.getRootURLContext(ldapURLContext.java:61)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:202)
at com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at org.apache.logging.log4j.core.net.JndiManager.lookup(JndiManager.java:172)
at org.apache.logging.log4j.core.lookup.JndiLookup.lookup(JndiLookup.java:56)
at org.apache.logging.log4j.core.lookup.Interpolator.lookup(Interpolator.java:221)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.resolveVariable(StrSubstitutor.java:1110)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:1033)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:912)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.replace(StrSubstitutor.java:467)
at org.apache.logging.log4j.core.pattern.MessagePatternConverter.format(MessagePatternConverter.java:132)
at org.apache.logging.log4j.core.pattern.PatternFormatter.format(PatternFormatter.java:38)
at org.apache.logging.log4j.core.layout.PatternLayout$PatternSerializer.toSerializable(PatternLayout.java:344)
at org.apache.logging.log4j.core.layout.PatternLayout.toText(PatternLayout.java:244)
at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:229)
at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:59)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.directEncodeEvent(AbstractOutputStreamAppender.java:197)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.tryAppend(AbstractOutputStreamAppender.java:190)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.append(AbstractOutputStreamAppender.java:181)
at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:156)
at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:129)
at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:120)
at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:84)
at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:540)
at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:498)
at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:481)
at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:456)
at org.apache.logging.log4j.core.config.AwaitCompletionReliabilityStrategy.log(AwaitCompletionReliabilityStrategy.java:82)
at org.apache.logging.log4j.core.Logger.log(Logger.java:161)
at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2205)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2159)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2142)
at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2017)
at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:1983)
at org.apache.logging.log4j.spi.AbstractLogger.error(AbstractLogger.java:740)
- 调用栈是最好的源码分析工具。
- 核心的关键类包括MessagePatternConverter、StrSubstitutor、JndiLookup、JndiManager。
源码分析
MessagePatternConverter
public final class MessagePatternConverter extends LogEventPatternConverter {
private MessagePatternConverter(final Configuration config, final String[] options) {
super("Message", "message");
this.formats = options;
this.config = config;
final int noLookupsIdx = loadNoLookups(options);
// Constants.FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS
// 变量代表log4j2.formatMsgNoLookups
this.noLookups = Constants.FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS || noLookupsIdx >= 0;
this.textRenderer = loadMessageRenderer(noLookupsIdx >= 0 ? ArrayUtils.remove(options, noLookupsIdx) : options);
}
public void format(final LogEvent event, final StringBuilder toAppendTo) {
final Message msg = event.getMessage();
if (msg instanceof StringBuilderFormattable) {
final boolean doRender = textRenderer != null;
// 省略多余的代码
// 根据 noLookups 的值避免执行该代码分支
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);
// config.getStrSubstitutor().replace(event, value)进行占位符替换
workingBuilder.append(config.getStrSubstitutor().replace(event, value));
}
}
}
// 省略多余的代码
return;
}
}
}
- config.getStrSubstitutor().replace(event, value)负责执行 jndi 命令的解析。
- 通过StrSubstitutor进行命令的解析。
- 通过变量noLookups来判定是否走代码分支,通过设置log4j2.formatMsgNoLookups为 true 不会走入该代码分支。
StrSubstitutor
public class StrSubstitutor implements ConfigurationAware {
protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf,
final int startPos, final int endPos) {
// Interpolator
final StrLookup resolver = getVariableResolver();
if (resolver == null) {
return null;
}
return resolver.lookup(event, variableName);
}
}
public class Interpolator extends AbstractConfigurationAwareLookup {
public static final char PREFIX_SEPARATOR = ':';
private static final String LOOKUP_KEY_WEB = "web";
private static final String LOOKUP_KEY_DOCKER = "docker";
private static final String LOOKUP_KEY_KUBERNETES = "kubernetes";
private static final String LOOKUP_KEY_SPRING = "spring";
private static final String LOOKUP_KEY_JNDI = "jndi";
private static final String LOOKUP_KEY_JVMRUNARGS = "jvmrunargs";
private static final Logger LOGGER = StatusLogger.getLogger();
private final Map<String, StrLookup> strLookupMap = new HashMap<>();
private final StrLookup defaultLookup;
@Override
public String lookup(final LogEvent event, String var) {
final int prefixPos = var.indexOf(PREFIX_SEPARATOR);
if (prefixPos >= 0) {
final String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);
final String name = var.substring(prefixPos + 1);
// 查找对应的StrLookup对象,这里返回的是 jndiLookup
final StrLookup lookup = strLookupMap.get(prefix);
if (lookup instanceof ConfigurationAware) {
((ConfigurationAware) lookup).setConfiguration(configuration);
}
String value = null;
if (lookup != null) {
// 执行jndiLookup的 lookup 方法
value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
}
if (value != null) {
return value;
}
}
}
}
- Interpolator#lookup内部 ladp 关键字从strLookupMap查找JndiLookup对象。
- 执行JndiLookup的 lookup 去解析 jndi 命令。
JndiLookup
public interface StrLookup {
String CATEGORY = "Lookup";
String lookup(String key);
String lookup(LogEvent event, String key);
}
public abstract class AbstractLookup implements StrLookup {
@Override
public String lookup(final String key) {
return lookup(null, key);
}
}
@Plugin(name = "jndi", category = StrLookup.CATEGORY)
public class JndiLookup extends AbstractLookup {
private static final Logger LOGGER = StatusLogger.getLogger();
private static final Marker LOOKUP = MarkerManager.getMarker("LOOKUP");
static final String CONTAINER_JNDI_RESOURCE_PATH_PREFIX = "java:comp/env/";
@Override
public String lookup(final LogEvent event, final String key) {
if (key == null) {
return null;
}
final String jndiName = convertJndiName(key);
try (final JndiManager jndiManager = JndiManager.getDefaultManager()) {
// 调用jndiManager的lookup方法
return Objects.toString(jndiManager.lookup(jndiName), null);
} catch (final NamingException e) {
return null;
}
}
}
- JndiLookup的 lookup 执行的是JndiManager的lookup。
JndiManager
public class JndiManager extends AbstractManager {
public <T> T lookup(final String name) throws NamingException {
// 这里的context为InitialContext
return (T) this.context.lookup(name);
}
}
public class InitialContext implements Context {
public Object lookup(String name) throws NamingException {
// 返回的ldapURLContext对象并执行 lookup操作。
return getURLOrDefaultInitCtx(name).lookup(name);
}
protected Context getURLOrDefaultInitCtx(String name)
throws NamingException {
if (NamingManager.hasInitialContextFactoryBuilder()) {
return getDefaultInitCtx();
}
// 返回ldapURLContext的对象
String scheme = getURLScheme(name);
if (scheme != null) {
Context ctx = NamingManager.getURLContext(scheme, myProps);
if (ctx != null) {
return ctx;
}
}
return getDefaultInitCtx();
}
}
- JndiManager的lookup会通过getURLOrDefaultInitCtx的返回ldapURLContext对象。
ldapURLContext
public final class ldapURLContext extends GenericURLDirContext {
public Object lookup(String var1) throws NamingException {
if (LdapURL.hasQueryComponents(var1)) {
throw new InvalidNameException(var1);
} else {
// 执行ldapURLContext的 lookup 方法
return super.lookup(var1);
}
}
}
- com.sun.jndi.url.ldap.ldapURLContext已经是 jdk 的源码,说明已经进入到 jdk 的执行逻辑。
- 整体的流程也就分析到此为止。