美文网首页log4j收藏
Log4j2漏洞分析源码篇

Log4j2漏洞分析源码篇

作者: 晴天哥_王志 | 来源:发表于2021-12-12 22:22 被阅读0次

    开篇

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

    调用栈

    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漏洞分析源码篇

        本文链接:https://www.haomeiwen.com/subject/rgulfrtx.html