Log4j2漏洞分析源碼篇

開(kāi)篇

?這篇文章簡(jiǎn)單的分析 Log4j2的臨時(shí)解決方案的思路,同時(shí)分析下 jndi攻擊的源碼代碼。臨時(shí)性解決方案的核心思路是:log4j2.formatMsgNoLookups設(shè)置為T(mén)rue。通過(guò)添加 -Dlog4j2.formatMsgNoLookups=true或創(chuàng)建 “l(fā)og4j2.component.properties” 文件并增加配置 “l(fā)og4j2.formatMsgNoLookups=true”。

調(diào)用棧

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)
  • 調(diào)用棧是最好的源碼分析工具。
  • 核心的關(guān)鍵類(lèi)包括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;
            // 省略多余的代碼

            // 根據(jù) noLookups 的值避免執(zhí)行該代碼分支
            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)進(jìn)行占位符替換
                        workingBuilder.append(config.getStrSubstitutor().replace(event, value));
                    }
                }
            }
            // 省略多余的代碼
            return;
        }
    }
}
  • config.getStrSubstitutor().replace(event, value)負(fù)責(zé)執(zhí)行 jndi 命令的解析。
  • 通過(guò)StrSubstitutor進(jìn)行命令的解析。
  • 通過(guò)變量noLookups來(lái)判定是否走代碼分支,通過(guò)設(shè)置log4j2.formatMsgNoLookups為 true 不會(huì)走入該代碼分支。


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);
            // 查找對(duì)應(yīng)的StrLookup對(duì)象,這里返回的是 jndiLookup
            final StrLookup lookup = strLookupMap.get(prefix);
            if (lookup instanceof ConfigurationAware) {
                ((ConfigurationAware) lookup).setConfiguration(configuration);
            }

            String value = null;
            if (lookup != null) {
                // 執(zhí)行jndiLookup的 lookup 方法
                value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
            }

            if (value != null) {
                return value;
            }
        }
    }
}
  • Interpolator#lookup內(nèi)部 ladp 關(guān)鍵字從strLookupMap查找JndiLookup對(duì)象。
  • 執(zhí)行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()) {
            // 調(diào)用jndiManager的lookup方法
            return Objects.toString(jndiManager.lookup(jndiName), null);
        } catch (final NamingException e) {
            return null;
        }
    }
}
  • JndiLookup的 lookup 執(zhí)行的是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對(duì)象并執(zhí)行 lookup操作。
        return getURLOrDefaultInitCtx(name).lookup(name);
    }

    protected Context getURLOrDefaultInitCtx(String name)
        throws NamingException {
        if (NamingManager.hasInitialContextFactoryBuilder()) {
            return getDefaultInitCtx();
        }

        // 返回ldapURLContext的對(duì)象
        String scheme = getURLScheme(name);
        if (scheme != null) {
            Context ctx = NamingManager.getURLContext(scheme, myProps);
            if (ctx != null) {
                return ctx;
            }
        }
        return getDefaultInitCtx();
    }
}
  • JndiManager的lookup會(huì)通過(guò)getURLOrDefaultInitCtx的返回ldapURLContext對(duì)象。


ldapURLContext

public final class ldapURLContext extends GenericURLDirContext {

    public Object lookup(String var1) throws NamingException {
        if (LdapURL.hasQueryComponents(var1)) {
            throw new InvalidNameException(var1);
        } else {
            // 執(zhí)行l(wèi)dapURLContext的 lookup 方法
            return super.lookup(var1);
        }
    }
}
  • com.sun.jndi.url.ldap.ldapURLContext已經(jīng)是 jdk 的源碼,說(shuō)明已經(jīng)進(jìn)入到 jdk 的執(zhí)行邏輯。
  • 整體的流程也就分析到此為止。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容