Log4j2漏洞分析源码篇

开篇

 这篇文章简单的分析 Log4j2的临时解决方案的思路,同时分析下 jndi攻击的源码代码。临时性解决方案的核心思路是:log4j2.formatMsgNoLookups设置为True。通过添加 -Dlog4j2.formatMsgNoLookups=true或创建 “log4j2.component.properties” 文件并增加配置 “log4j2.formatMsgNoLookups=true”。

Log4j2漏洞分析源码篇

调用栈

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 的执行逻辑。
  • 整体的流程也就分析到此为止。
上一篇:Log4j2高危漏洞复现流程


下一篇:当服务QPS增高时,我们做什么