log4j2 JNDI 注入漏洞分析

本文最后更新于 2021.12.11,总计 5869 字 ,阅读本文大概需要 10 ~ 37 分钟
本文已超过 867天 没有更新。如果文章内容或图片资源失效,请留言反馈,我会及时处理,谢谢!

0x01 写在前面

2021 年 12 月 9 号注定是一个不眠之夜,著名的Apache Log4j 项目被爆存在远程代码执行漏洞,且利用简单,影响危害巨大,光是引入了 log4j2 依赖的组件都是数不清,更别提项目本身可能存在的风险了,如下图所示,mvnrepository搜索引用了 log4j-core version 2.14.1的项目就 十几页了:

1.png

本文就来简单分析一下该漏洞的原理。

0x02 影响范围

引用了版本处于2.x < 2.15.0-rc2的 Apache log4j-core的应用项目或组件

0x03 漏洞分析

根据官方的修订信息:https://issues.apache.org/jira/projects/LOG4J2/issues/LOG4J2-3201?filter=allissues

2.png

可以明确知道,是通过 jndi 中 LDAP 注入的方式实现了 RCE,然后查看其补丁的更改记录:

3.png

可以发现对lookup函数进行了修改判断

知道了漏洞类型,那么就好入手了,首先翻阅官方文档中关于lookup的说明:

4.png

lookup提供了一种在任意位置向 Log4j2 配置添加值的方法,是实现StrLookup接口的特殊类型的插件 ,查看官方文档发现log4j2 支持的方法有很多:

5.png

总计有:base64datactxmainenvsyssdjavamarkerjndijvmrunargsmapbundlelog4j

由于这里主要说明的是关于 JNDI lookup 的用法,其他的不再赘述。

关于 JNDI lookup官方文档有说明:

6.png

JndiLookup 允许通过 JNDI 检索变量,然后给了示例:

<File name="Application" fileName="application.log">
    <PatternLayout>
        <pattern>%d %p %c{1.} [%t] $${jndi:logging/context-name} %m%n</pattern>
    </PatternLayout>
</File>

实际上通过 log4j2 支持的方法那张图中就可以发现log4j 中 jdni 的用法格式如下:

${jndi:JNDIContent}

既然明确了lookup是触发漏洞的点,并且找到了可以触发 lookup的方法 ,那么就可以找入口点,只要找到入口点,然后传入 jndi 调用 ldap 的方式,就能够实现 RCE。

那么,哪一个入口点可以传入${jndi:JNDIContent}呢?

没错了,就是LogManager.getLogger().xxxx()方法

在log4j2中,共有8 个日志级别,可以通过LogManager.getLogger()调用记录日志的方法如下:

LogManager.getLogger().error()
LogManager.getLogger().fatal()

LogManager.getLogger().trace()
LogManager.getLogger().traceExit()
LogManager.getLogger().traceEntry()
LogManager.getLogger().info()
LogManager.getLogger().warn()
LogManager.getLogger().debug()
LogManager.getLogger().log()
LogManager.getLogger().printf()

上述列表中,error()fatal()方法可默认触发漏洞,其余的方法需要配置日志级别才可以触发漏洞。因为在logIfEnabled方法中,对当前日志等级进行了一次判断:

7.png

只有当当前事件的日志等级大于等于设置的日志等级时,才会符合条件,进入logMessage()方法

8.png

知道这些基本信息后,就可以进一步了解漏洞的触发原理了。

测试 case 如下:

public class log4j {  
  
    private static final Logger logger = LogManager.getLogger();  
  
    public static void main(String[] args) {  
        Collection<org.apache.logging.log4j.core.Logger> current = LoggerContext.getContext(false).getLoggers();  
        Collection<org.apache.logging.log4j.core.Logger> notcurrent = LoggerContext.getContext().getLoggers();  
        Collection<org.apache.logging.log4j.core.Logger> allConfig = current;  
        allConfig.addAll(notcurrent);  
        for (org.apache.logging.log4j.core.Logger log:allConfig){  
            log.setLevel(Level.ALL);  
        }  
        logger.error(Level.ALL,"payload");  
//        logger.warn("payload");  
//        logger.info("payload");  
//        logger.debug("payload");  
//        logger.traceExit("payload");  
//        logger.trace("payload");  
//        logger.fatal("payload");
//        logger.printf(Level.ALL,"payload");  
//        logger.traceEntry("payload");  
//        logger.log(Level.ALL,"payload");  
 }    
}

由于这些调用方法触发漏洞的原理都是一样的,所以本文就以 error 举例说明。

查看 error 的类继承关系可以发现,实际上会调用AbstractLogger.java中的public void error()方法:

9.png

在该方法中会调用logIfEnabled判断是否符合日志记录的等级要求,如果符合,那么会进行logMessage操作:

10.png

后续不关键调用路径如下:

logMessage ----> logMessageSafely ----> logMessageTrackRecursion ----> tryLogMessage ----> log

----> DefaultReliabilityStrategy.log ----> loggerConfig.log ----> processLogEvent ----> callAppenders
----> tryCallAppender ----> append ----> tryAppend ----> directEncodeEvent ----> encode ----> toText ---->
toSerializable ---->format----> PatternFormatter.format

第一个关键点在PatternFormatter.java中的 format方法:

11.png

如果检测到$字符后跟了一个{字符,那么会对直到}中间的内容进行解析并replace

replace --> substitute --> StrSubstitutor.substitute --> resolveVariable --> Interpolator.lookup

Interpolator.lookup方法中,首先会获取字符串的前缀值:

12.png

如果匹配到内置方法,那么就进入对应的处理方法,这里是 JNDI 方法,那么就会由JndiLookup类进一步处理:

13.png

最终加载由攻击者传入的LDAP服务端地址,然后返回一个恶意的JNDI Reference对象,触发漏洞,实现 RCE。

0x04 漏洞复现

14.png

0x05 写在最后

log4j2涉及的组件之多、牵扯的范围之广,造成的结果,恐怕是漏洞发现者或者是某个公开 poc 的安全公众号都始料未及的。
其实这个漏洞带给我们的不仅仅是一个新的吃饭技能,更多的是一些思考:

第一,安全发展至今,为什么一个 java底层依赖出现漏洞,却导致国内所有大厂全军覆没?虽然个别厂商提前修复,但同样说明了一件事,供应链级别的0day攻击,依旧是无法第一时间防御的。是否存在一种新的机制或方案能够防御供应链级别的0day攻击?

第二,log4j2 项目引入JNDI lookup 已有 7 年之久,但是应用范围如此之广、利用复杂难度之低的漏洞,长达 7 年未被发现,实在有些惭愧(更惭愧的是官方文档还有 jndilookp 的使用说明)

第三,以后挖0day思路真如 skay 说的一样加一:
find *.jar => add as library => shift+shift => find log4j => RCE
实际上不仅是 log4j,通常这也是一个挖掘组件依赖漏洞的一种思路

第四
15.png

参考

https://logging.apache.org/log4j/2.x/manual/lookups.html
https://github.com/apache/logging-log4j2/pull/608/commits/755e2c9d57f0517a73d16bfcaed93cc91969bdee

「感谢老板送来的软糖/蛋糕/布丁/牛奶/冰阔乐!」

panda

(๑>ڡ<)☆谢谢老板~

使用微信扫描二维码打赏

版权属于:

panda | 热爱安全的理想少年

本文链接:

https://cnpanda.net/sec/1114.html(转载时请注明本文出处及文章链接)

暂时无法评论哦~

暂无评论