# REPRO-2026-00340: PaperCut NG/MF CVE-2026-81578 + CVE-2026-82078 unauthenticated RCE chain ## Summary Status: published Severity: critical CVSS: 9.4 / 10 CWE: CWE-284 Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00340 CVE: CVE-2026-81578 ## Package Name: PaperCut NG/MF Ecosystem: vendor Affected: 25.0.11 build 75758 (tested) Fixed: 25.0.12 build 76510 (Emergency Patch Release 2) ## Root Cause # Root Cause Analysis — PaperCut NG/MF 25.0.11 unauthenticated remote code execution (27 Aug 2026 advisory) ## Summary Stock PaperCut NG/MF 25.0.11 (build 75758) allows a fully unauthenticated remote attacker to execute operating-system commands as the PaperCut application-service account (`papercut`). The chain combines two defects reachable through the pre-authentication application surface of the running Application Server: 1. **Tapestry 3 "complex" direct-service authorization bypass.** Tapestry's `DirectService` accepts `service=direct/1///`. It calls `validate()` (which fires PaperCut's `BasePaperCutPage.pageValidate` access-rights check) **only on the render page**, then triggers the listener on the *component page* without validating it. Using the public login page as render page (`Home`) and a privileged page as component page (`ConfigEditor`) yields unauthenticated arbitrary configuration writes (`quickFindForm` + `$Form`/`$Form$0` listeners invoke `ConfigManager.setString`). 2. **Unrestricted attacker-controlled SQL in the external Card/ID lookup.** `ExternalUserLookupDb.lookupUserByExternalCardNumber` opens a JDBC connection using the `user-lookup.db-driver` / `user-lookup.db-url` config keys and runs the `user-lookup.id-to-username-sql` template with the submitted card number bound as a parameter. With the bundled Apache Derby embedded driver the template may call `SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE`, giving an attacker-controlled arbitrary file write as the `papercut` user. The lookup is reached **before any login** through the web card/ID login flow (`auth.web-login.card-id.enable`) — `Home.login` → `AuthenticationManagerImpl.authenticateUserWithCard` → `UserManagerImpl.getUserByCardNumber` → `ExternalUserLookupManagerImpl` → `ExternalUserLookupDb`. File write is escalated to in-JVM code execution by planting a new Tapestry page (`WEB-INF/Pwn3.page` declaring `org.apache.tapestry.html.BasePage`, plus `Pwn3.html` containing an OGNL expression binding) into the live Jetty webapp extraction directory (`server/tmp/webapp-/`), then requesting `GET /app?service=page/Pwn3`. Tapestry loads the new page specification from the servlet context on first access and evaluates the OGNL expression (`@java.lang.Runtime@getRuntime().exec(...)`), executing the command in the Application Server JVM. The command's output is written into the webroot and fetched back over plain HTTP — a complete remote command receipt. The emergency 25.0.12 (build 76497) patch blocks the chain at the SQL layer: `ExternalUserLookupDb.createLookupSQLStatement` now rejects templates matching `(?i)\b(CALL|EXEC|EXECUTE)\b|\bSYSCS_[A-Z0-9_]*\b` with `ApplicationException("Unsafe external user lookup SQL blocked.")`. ## Impact - Package/component: PaperCut NG (and MF) Application Server, all 25.0.x <= 25.0.11 (and per the vendor advisory, effectively all supported branches until the emergency builds). - Risk: critical. Unauthenticated remote OS command execution as the PaperCut service account from the web port (9191/9192), including from the internet if exposed. Post-exploitation runs as `pc-app` child processes, matching the vendor's IoCs. ## Impact Parity - Disclosed/claimed maximum impact: unauthenticated remote code execution as the PaperCut application-service account via a pre-auth card/ID entrypoint. - Reproduced impact from this run: exactly that — `uid=1001(papercut) gid=1001(papercut)` command output (`id; uname -a; cat /proc/1/comm`) executed inside the Application Server container and retrieved remotely over HTTP. - Parity: **full**. - Nothing claimed was left undemonstrated. ## Root Cause Two chained defects: 1. `biz/papercut/pcng/web/pages/BasePaperCutPage.pageValidate` (25.0.11) only computes access rights for `this` — the page being validated. Tapestry 3's `DirectService.service()` validates only the *render* page (`cycle.activate(pageName)`) but then calls `componentPage.getNestedComponent (componentPath)` and `direct.trigger(cycle)` on a second page named later in the service path. `POST /app?service=direct/1/Home/ConfigEditor/$Form$0` (and `/quickFindForm`, `/$Form`) therefore fires ConfigEditor's form listeners (`doAddNew`, `doConfigEdit` → `ConfigManager.setString`) with no session at all. The 25.0.12 patch adds a loop over `service.split("/")` accumulating the rights of every embedded page — but note this check still only runs in `BasePaperCutPage.pageValidate`, and `Home` *overrides* `pageValidate` without calling `super`, so the config-write bypass via `direct/1/Home/...` in fact still lands on 25.0.12 (observed at runtime; the RCE chain is nonetheless blocked by the SQL filter below). 2. `biz/papercut/pcng/service/impl/ExternalUserLookupDb` built a `PreparedStatement` directly from the `user-lookup.id-to-username-sql` config value with no statement-type restriction, and opened the connection from config-controlled driver/URL (`DatabaseUtils.openConnection` → `Class.forName` + `DriverManager.getConnection`). Because the whole statement is attacker-controlled and the bundled Derby embedded driver supports `CALL SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE(?, ...)`, the card number (bound to the `?`) becomes an arbitrary SQL query whose CLOB result is written verbatim to an arbitrary filesystem path. The advisory IoCs fall out of this naturally: failed attempts log `Database error looking up cardID: VALUES CAST...` and driver probing logs `No suitable driver found for jdbc:...`. Fix commit: no public commit (commercial product). Vendor advisory: `https://www.papercut.com/kb/Main/security-bulletin-27-aug-2026-urgent-security-advisory` (emergency builds 25.0.12 build 76497 / 26.x; the FAQ documents the new EXEC/EXECUTE/CALL restriction). ## Reproduction Steps 1. `bundle/repro/reproduction_steps.sh` (self-contained; requires docker + curl + python3; downloads the two official installers with vendor-published SHA-256 verification). 2. The script builds `pcng-vuln` (25.0.11.75758) and `pcng-fixed` (25.0.12.76497) images, starts both Application Servers, completes the stock setup wizard on each, performs one operator-side admin login + ConfigEditor render per server (models a normally operated server; warms Tapestry's pooled page/table state that the form-rewind path needs), and then runs the entire attack with **unauthenticated remote HTTP requests only**: - Phase A1: config write via `service=direct/1/Home/ConfigEditor/...` (arms `user-lookup.*` and `auth.web-login.card-id.*`). - Phase A2: two pre-auth card-login requests whose card number is `VALUES(CAST('' AS CLOB))`; the Derby export plants `WEB-INF/Pwn3.page` and `Pwn3.html` (OGNL payload) in the live webapp dir. - Phase A3: `GET /app?service=page/Pwn3` evaluates the OGNL payload. - Phase A4: `GET /pwn-proof.txt` retrieves the command output (remote receipt). - Phase B: identical request sequence against 25.0.12; verifies the armed lookup reaches the new filter (`Unsafe external user lookup SQL blocked`), no files are planted, no command receipt exists, and a benign non-CALL SQL template still executes (proving the feature works and only dangerous SQL is blocked). 3. Expected evidence: vuln proof file containing `PC0DAY-PROOF-BEGIN ... uid=1001(papercut) ... PC0DAY-PROOF-END` fetched over HTTP; fixed build logs the SQL block and returns 404 for the proof file. Exit code 0 = chain proven on 25.0.11 and blocked on 25.0.12. ## Evidence - Full run log: `bundle/logs/reproduction_steps.log` (two consecutive clean runs, both exit 0). - Remote command receipt (fetched over HTTP): `bundle/artifacts/vuln-proof.txt`. - Planted-page render response: `bundle/artifacts/vuln-render.html`. - Vulnerable server.log IoC excerpts (attacker payload visible in `Database error looking up cardID: VALUES(CAST(...)`): `bundle/artifacts/vuln-serverlog-ioc.txt`. - Fixed-build server.log excerpts showing `Unsafe external user lookup SQL blocked` plus the benign-SQL Derby error: `bundle/artifacts/fixed-serverlog-ioc.txt`. - Per-request transcripts: `bundle/artifacts/*-wiz*.html(.headers)`, `bundle/artifacts/fixed-configwrite.html(.headers)`, `bundle/artifacts/vuln-proof.headers`, etc. - Build identities: `bundle/artifacts/vuln-version.txt`, `bundle/artifacts/fixed-version.txt`; installer SHA-256s verified against the vendor advisory page (vuln 64495771…817a, fixed 0782c1d6…c392). - Runtime manifest: `bundle/repro/runtime_manifest.json`. - Patch localization (25.0.11 vs 25.0.12 class diff): `ExternalUserLookupDb` (SQL filter — the effective fix), `BasePaperCutPage` (service-path rights), `StandardRhinoContextFactory` (new Rhino ClassShutter blocking `java.lang.Class/ClassLoader/reflect/invoke`), `WebConfig` (path matching / response headers), `RestrictApiAccessFilter` (URI canonicalization). ## Recommendations / Next Steps - Upgrade to the emergency build (25.0.12 / 26.0.3+) per the vendor advisory. - The 25.0.12 fix blocks the RCE at the SQL layer, but the unauthenticated ConfigEditor write via Tapestry complex direct service was still observable on 25.0.12 in this lab (Home overrides `pageValidate` without invoking the new service-path check). Recommend PaperCut route the rights check through a path that Home cannot skip (e.g., validate the component page in `DirectService` itself or move checks into a servlet filter), and audit every privileged page listener for the same pattern. - Defense-in-depth: disallow `CALL`/`EXEC`/`SYSCS_` (already patched), consider allowing only plain `SELECT` templates; do not let `user-lookup.db-driver` accept arbitrary driver class names; disallow absolute paths in Derby export procedures via a Java SecurityManager-equivalent policy is no longer possible on JDK 21, so statement allow-listing is the right layer. - Network-level: keep the Application Server web interface off untrusted networks (vendor's immediate guidance). ## Additional Notes - Idempotency: the script was run end-to-end twice consecutively (fresh containers each time), both runs exited 0. Derby export procedures refuse to overwrite existing files, so each run uses fresh output paths (`/tmp/pc0day-a.csv`, `pc0day-b.csv`, unique per run) and fresh containers. - Precondition: the Tapestry form-rewind path used for the config write relies on the pooled ConfigEditor page/table state, which exists after the Config Editor page has been rendered once since server start (any normal admin UI usage). The script performs this as an operator-side action (setup wizard admin account); it is not part of the attacker's request sequence. On a production server that has ever opened Options → Config Editor this precondition holds naturally. - The card number is bound as a JDBC parameter (`?`), so no SQL-escaping of the payload is required; the entire export query rides in the card value. Content written to files must avoid single quotes or use Derby literal doubling; the OGNL payload sidesteps this by constructing strings from byte arrays. - The print/device-script (Rhino) route was not needed; 25.0.12 additionally shutters Rhino reflection (`StandardRhinoContextFactory`), closing the alternate script-based escalation. ## Reproduction Details Reproduced: 2026-08-31T05:51:19.235Z Duration: 9828 seconds Tool calls: 531 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00340 pruva-verify CVE-2026-81578 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00340&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00340/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-81578 - Source: https://www.papercut.com/kb/Main/security-bulletin-27-aug-2026-urgent-security-advisory/ ## Artifacts - bundle/repro/rca_report.md (analysis, 11544 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 23600 bytes) - bundle/artifacts/fixed-proof.txt (other, 1442 bytes) - bundle/artifacts/fixed-render.html (other, 11360 bytes) - bundle/artifacts/fixed-version.txt (other, 165 bytes) - bundle/artifacts/fixed-wiz1.html (other, 5851 bytes) - bundle/artifacts/fixed-wiz1.html.headers (other, 229 bytes) - bundle/artifacts/fixed-wiz2.html (other, 5846 bytes) - bundle/artifacts/fixed-wiz2.html.headers (other, 229 bytes) - bundle/artifacts/fixed-wiz3.html (other, 5824 bytes) - bundle/artifacts/fixed-wiz3.html.headers (other, 229 bytes) - bundle/artifacts/fixed-wiz4.html (other, 7568 bytes) - bundle/artifacts/fixed-wiz4.html.headers (other, 229 bytes) - bundle/artifacts/fixed-wiz5.html (other, 6154 bytes) - bundle/artifacts/fixed-wiz5.html.headers (other, 229 bytes) - bundle/artifacts/fixed-wiz6.html (other, 6028 bytes) - bundle/artifacts/fixed-wiz6.html.headers (other, 229 bytes) - bundle/artifacts/fixed-wiz7.html (other, 2629 bytes) - bundle/artifacts/fixed-wiz7.html.headers (other, 418 bytes) - bundle/artifacts/vuln-proof.headers (other, 229 bytes) - bundle/artifacts/vuln-render.headers (other, 229 bytes) - bundle/artifacts/vuln-version.txt (other, 165 bytes) - bundle/artifacts/vuln-wiz1.html (other, 5851 bytes) - bundle/artifacts/vuln-wiz1.html.headers (other, 229 bytes) - bundle/artifacts/vuln-wiz2.html (other, 5846 bytes) - bundle/artifacts/vuln-wiz2.html.headers (other, 229 bytes) - bundle/artifacts/vuln-wiz3.html (other, 5824 bytes) - bundle/artifacts/vuln-wiz3.html.headers (other, 229 bytes) - bundle/artifacts/vuln-wiz4.html (other, 7568 bytes) - bundle/artifacts/vuln-wiz4.html.headers (other, 229 bytes) - bundle/artifacts/vuln-wiz5.html (other, 6154 bytes) - bundle/artifacts/vuln-wiz5.html.headers (other, 229 bytes) - bundle/artifacts/vuln-wiz6.html (other, 6028 bytes) - bundle/artifacts/vuln-wiz6.html.headers (other, 229 bytes) - bundle/artifacts/vuln-wiz7.html (other, 2629 bytes) - bundle/artifacts/vuln-wiz7.html.headers (other, 416 bytes) - bundle/repro/derive_attack.sh (other, 7769 bytes) - bundle/repro/runtime_manifest.json (other, 6655 bytes) - bundle/repro/validation_verdict.json (other, 1816 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00340 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00340/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00340 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev