{"repro_id":"REPRO-2026-00340","version":6,"title":"PaperCut NG/MF CVE-2026-81578 + CVE-2026-82078 unauthenticated RCE chain","repro_type":"security","status":"published","severity":"critical","cvss_score":9.4,"description":"Prove or reject unauthenticated remote OS command execution as the PaperCut service account on stock PaperCut NG/MF 25.0.11 via a pre-auth Release Station / MFD card-ID flow reaching the external card-lookup runtime, then replay identically on stock 25.0.12 to confirm the fix blocks the chain. This maps to the PaperCut NG/MF URGENT Security Bulletin of 27 Aug 2026 (active exploitation, all versions affected). Whether a stock remote mechanism exists is an open question; card-ID SQL injection and JDBC URL injection are unproven hypotheses only.","root_cause":"# Root Cause Analysis — PaperCut NG/MF 25.0.11 unauthenticated remote code execution (27 Aug 2026 advisory)\n\n## Summary\n\nStock PaperCut NG/MF 25.0.11 (build 75758) allows a fully unauthenticated remote\nattacker to execute operating-system commands as the PaperCut application-service\naccount (`papercut`). The chain combines two defects reachable through the\npre-authentication application surface of the running Application Server:\n\n1. **Tapestry 3 \"complex\" direct-service authorization bypass.** Tapestry's\n   `DirectService` accepts `service=direct/1/<RenderPage>/<ComponentPage>/<Component>`.\n   It calls `validate()` (which fires PaperCut's `BasePaperCutPage.pageValidate`\n   access-rights check) **only on the render page**, then triggers the listener on\n   the *component page* without validating it. Using the public login page as\n   render page (`Home`) and a privileged page as component page\n   (`ConfigEditor`) yields unauthenticated arbitrary configuration writes\n   (`quickFindForm` + `$Form`/`$Form$0` listeners invoke `ConfigManager.setString`).\n2. **Unrestricted attacker-controlled SQL in the external Card/ID lookup.**\n   `ExternalUserLookupDb.lookupUserByExternalCardNumber` opens a JDBC connection\n   using the `user-lookup.db-driver` / `user-lookup.db-url` config keys and runs\n   the `user-lookup.id-to-username-sql` template with the submitted card number\n   bound as a parameter. With the bundled Apache Derby embedded driver the\n   template may call `SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE`, giving an\n   attacker-controlled arbitrary file write as the `papercut` user. The lookup is\n   reached **before any login** through the web card/ID login flow\n   (`auth.web-login.card-id.enable`) — `Home.login` →\n   `AuthenticationManagerImpl.authenticateUserWithCard` →\n   `UserManagerImpl.getUserByCardNumber` → `ExternalUserLookupManagerImpl` →\n   `ExternalUserLookupDb`.\n\nFile write is escalated to in-JVM code execution by planting a new Tapestry page\n(`WEB-INF/Pwn3.page` declaring `org.apache.tapestry.html.BasePage`, plus\n`Pwn3.html` containing an OGNL expression binding) into the live Jetty webapp\nextraction directory (`server/tmp/webapp-<build>/`), then requesting\n`GET /app?service=page/Pwn3`. Tapestry loads the new page specification from the\nservlet context on first access and evaluates the OGNL expression\n(`@java.lang.Runtime@getRuntime().exec(...)`), executing the command in the\nApplication Server JVM. The command's output is written into the webroot and\nfetched back over plain HTTP — a complete remote command receipt.\n\nThe emergency 25.0.12 (build 76497) patch blocks the chain at the SQL layer:\n`ExternalUserLookupDb.createLookupSQLStatement` now rejects templates matching\n`(?i)\\b(CALL|EXEC|EXECUTE)\\b|\\bSYSCS_[A-Z0-9_]*\\b` with\n`ApplicationException(\"Unsafe external user lookup SQL blocked.\")`.\n\n## Impact\n\n- Package/component: PaperCut NG (and MF) Application Server, all 25.0.x <= 25.0.11\n  (and per the vendor advisory, effectively all supported branches until the\n  emergency builds).\n- Risk: critical. Unauthenticated remote OS command execution as the PaperCut\n  service account from the web port (9191/9192), including from the internet if\n  exposed. Post-exploitation runs as `pc-app` child processes, matching the\n  vendor's IoCs.\n\n## Impact Parity\n\n- Disclosed/claimed maximum impact: unauthenticated remote code execution as the\n  PaperCut application-service account via a pre-auth card/ID entrypoint.\n- Reproduced impact from this run: exactly that — `uid=1001(papercut)\n  gid=1001(papercut)` command output (`id; uname -a; cat /proc/1/comm`) executed\n  inside the Application Server container and retrieved remotely over HTTP.\n- Parity: **full**.\n- Nothing claimed was left undemonstrated.\n\n## Root Cause\n\nTwo chained defects:\n\n1. `biz/papercut/pcng/web/pages/BasePaperCutPage.pageValidate` (25.0.11) only\n   computes access rights for `this` — the page being validated. Tapestry 3's\n   `DirectService.service()` validates only the *render* page\n   (`cycle.activate(pageName)`) but then calls `componentPage.getNestedComponent\n   (componentPath)` and `direct.trigger(cycle)` on a second page named later in\n   the service path. `POST /app?service=direct/1/Home/ConfigEditor/$Form$0` (and\n   `/quickFindForm`, `/$Form`) therefore fires ConfigEditor's form listeners\n   (`doAddNew`, `doConfigEdit` → `ConfigManager.setString`) with no session at\n   all. The 25.0.12 patch adds a loop over `service.split(\"/\")` accumulating the\n   rights of every embedded page — but note this check still only runs in\n   `BasePaperCutPage.pageValidate`, and `Home` *overrides* `pageValidate`\n   without calling `super`, so the config-write bypass via `direct/1/Home/...`\n   in fact still lands on 25.0.12 (observed at runtime; the RCE chain is\n   nonetheless blocked by the SQL filter below).\n2. `biz/papercut/pcng/service/impl/ExternalUserLookupDb` built a\n   `PreparedStatement` directly from the `user-lookup.id-to-username-sql` config\n   value with no statement-type restriction, and opened the connection from\n   config-controlled driver/URL (`DatabaseUtils.openConnection` →\n   `Class.forName` + `DriverManager.getConnection`). Because the whole statement\n   is attacker-controlled and the bundled Derby embedded driver supports\n   `CALL SYSCS_UTIL.SYSCS_EXPORT_QUERY_LOBS_TO_EXTFILE(?, ...)`, the card number\n   (bound to the `?`) becomes an arbitrary SQL query whose CLOB result is written\n   verbatim to an arbitrary filesystem path. The advisory IoCs fall out of this\n   naturally: failed attempts log `Database error looking up cardID: VALUES CAST...`\n   and driver probing logs `No suitable driver found for jdbc:...`.\n\nFix commit: no public commit (commercial product). Vendor advisory:\n`https://www.papercut.com/kb/Main/security-bulletin-27-aug-2026-urgent-security-advisory`\n(emergency builds 25.0.12 build 76497 / 26.x; the FAQ documents the new\nEXEC/EXECUTE/CALL restriction).\n\n## Reproduction Steps\n\n1. `bundle/repro/reproduction_steps.sh` (self-contained; requires docker + curl\n   + python3; downloads the two official installers with vendor-published\n   SHA-256 verification).\n2. The script builds `pcng-vuln` (25.0.11.75758) and `pcng-fixed`\n   (25.0.12.76497) images, starts both Application Servers, completes the stock\n   setup wizard on each, performs one operator-side admin login + ConfigEditor\n   render per server (models a normally operated server; warms Tapestry's pooled\n   page/table state that the form-rewind path needs), and then runs the entire\n   attack with **unauthenticated remote HTTP requests only**:\n   - Phase A1: config write via `service=direct/1/Home/ConfigEditor/...`\n     (arms `user-lookup.*` and `auth.web-login.card-id.*`).\n   - Phase A2: two pre-auth card-login requests whose card number is\n     `VALUES(CAST('<file content>' AS CLOB))`; the Derby export plants\n     `WEB-INF/Pwn3.page` and `Pwn3.html` (OGNL payload) in the live webapp dir.\n   - Phase A3: `GET /app?service=page/Pwn3` evaluates the OGNL payload.\n   - Phase A4: `GET /pwn-proof.txt` retrieves the command output (remote\n     receipt).\n   - Phase B: identical request sequence against 25.0.12; verifies the armed\n     lookup reaches the new filter (`Unsafe external user lookup SQL blocked`),\n     no files are planted, no command receipt exists, and a benign non-CALL SQL\n     template still executes (proving the feature works and only dangerous SQL\n     is blocked).\n3. Expected evidence: vuln proof file containing\n   `PC0DAY-PROOF-BEGIN ... uid=1001(papercut) ... PC0DAY-PROOF-END` fetched over\n   HTTP; fixed build logs the SQL block and returns 404 for the proof file.\n\nExit code 0 = chain proven on 25.0.11 and blocked on 25.0.12.\n\n## Evidence\n\n- Full run log: `bundle/logs/reproduction_steps.log` (two consecutive clean\n  runs, both exit 0).\n- Remote command receipt (fetched over HTTP): `bundle/artifacts/vuln-proof.txt`.\n- Planted-page render response: `bundle/artifacts/vuln-render.html`.\n- Vulnerable server.log IoC excerpts (attacker payload visible in\n  `Database error looking up cardID: VALUES(CAST(...)`):\n  `bundle/artifacts/vuln-serverlog-ioc.txt`.\n- Fixed-build server.log excerpts showing\n  `Unsafe external user lookup SQL blocked` plus the benign-SQL Derby error:\n  `bundle/artifacts/fixed-serverlog-ioc.txt`.\n- Per-request transcripts: `bundle/artifacts/*-wiz*.html(.headers)`,\n  `bundle/artifacts/fixed-configwrite.html(.headers)`,\n  `bundle/artifacts/vuln-proof.headers`, etc.\n- Build identities: `bundle/artifacts/vuln-version.txt`,\n  `bundle/artifacts/fixed-version.txt`; installer SHA-256s verified against the\n  vendor advisory page (vuln 64495771…817a, fixed 0782c1d6…c392).\n- Runtime manifest: `bundle/repro/runtime_manifest.json`.\n- Patch localization (25.0.11 vs 25.0.12 class diff): `ExternalUserLookupDb`\n  (SQL filter — the effective fix), `BasePaperCutPage` (service-path rights),\n  `StandardRhinoContextFactory` (new Rhino ClassShutter blocking\n  `java.lang.Class/ClassLoader/reflect/invoke`), `WebConfig` (path matching /\n  response headers), `RestrictApiAccessFilter` (URI canonicalization).\n\n## Recommendations / Next Steps\n\n- Upgrade to the emergency build (25.0.12 / 26.0.3+) per the vendor advisory.\n- The 25.0.12 fix blocks the RCE at the SQL layer, but the unauthenticated\n  ConfigEditor write via Tapestry complex direct service was still observable on\n  25.0.12 in this lab (Home overrides `pageValidate` without invoking the new\n  service-path check). Recommend PaperCut route the rights check through a path\n  that Home cannot skip (e.g., validate the component page in `DirectService`\n  itself or move checks into a servlet filter), and audit every privileged page\n  listener for the same pattern.\n- Defense-in-depth: disallow `CALL`/`EXEC`/`SYSCS_` (already patched), consider\n  allowing only plain `SELECT` templates; do not let `user-lookup.db-driver`\n  accept arbitrary driver class names; disallow absolute paths in Derby export\n  procedures via a Java SecurityManager-equivalent policy is no longer possible\n  on JDK 21, so statement allow-listing is the right layer.\n- Network-level: keep the Application Server web interface off untrusted\n  networks (vendor's immediate guidance).\n\n## Additional Notes\n\n- Idempotency: the script was run end-to-end twice consecutively (fresh\n  containers each time), both runs exited 0. Derby export procedures refuse to\n  overwrite existing files, so each run uses fresh output paths\n  (`/tmp/pc0day-a.csv`, `pc0day-b.csv`, unique per run) and fresh containers.\n- Precondition: the Tapestry form-rewind path used for the config write relies\n  on the pooled ConfigEditor page/table state, which exists after the Config\n  Editor page has been rendered once since server start (any normal admin UI\n  usage). The script performs this as an operator-side action (setup wizard\n  admin account); it is not part of the attacker's request sequence. On a\n  production server that has ever opened Options → Config Editor this\n  precondition holds naturally.\n- The card number is bound as a JDBC parameter (`?`), so no SQL-escaping of the\n  payload is required; the entire export query rides in the card value. Content\n  written to files must avoid single quotes or use Derby literal doubling; the\n  OGNL payload sidesteps this by constructing strings from byte arrays.\n- The print/device-script (Rhino) route was not needed; 25.0.12 additionally\n  shutters Rhino reflection (`StandardRhinoContextFactory`), closing the\n  alternate script-based escalation.\n","cve_id":"CVE-2026-81578","cwe_id":"CWE-284","source_url":"https://www.papercut.com/kb/Main/security-bulletin-27-aug-2026-urgent-security-advisory/","package":{"name":"PaperCut NG/MF","ecosystem":"vendor","affected_versions":"25.0.11 build 75758 (tested)","fixed_version":"25.0.12 build 76510 (Emergency Patch Release 2)","tested_vulnerable":"25.0.11 build 75758","tested_patched":"25.0.12 build 76497 (Release 1: RCE blocked; residual access-control bypass confirmed)"},"reproduced_at":"2026-08-31T05:51:19.235792+00:00","duration_secs":9828.0,"tool_calls":531,"handoffs":2,"total_cost_usd":27.174203,"agent_costs":{"claim_matcher":0.033826,"judge":0.899435,"learning_policy":0.017015,"repro":21.632814,"support":0.131735,"vuln_variant":4.459378},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.033826},"judge":{"gpt-5.6-sol":0.899435},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.017015},"repro":{"accounts/fireworks/models/kimi-k3":21.632814},"support":{"accounts/fireworks/models/kimi-k3":0.131735},"vuln_variant":{"accounts/fireworks/models/kimi-k3":4.459378}},"vulnerable_version_variant_outcome":"unknown","fix_bypass_outcome":"unknown","variant_disclosure_state":"unknown","quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-08-31T05:51:20.543065+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":11544,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":23600,"category":"reproduction_script"},{"path":"bundle/artifacts/fixed-proof.txt","filename":"fixed-proof.txt","size":1442,"category":"other"},{"path":"bundle/artifacts/fixed-render.html","filename":"fixed-render.html","size":11360,"category":"other"},{"path":"bundle/artifacts/fixed-version.txt","filename":"fixed-version.txt","size":165,"category":"other"},{"path":"bundle/artifacts/fixed-wiz1.html","filename":"fixed-wiz1.html","size":5851,"category":"other"},{"path":"bundle/artifacts/fixed-wiz1.html.headers","filename":"fixed-wiz1.html.headers","size":229,"category":"other"},{"path":"bundle/artifacts/fixed-wiz2.html","filename":"fixed-wiz2.html","size":5846,"category":"other"},{"path":"bundle/artifacts/fixed-wiz2.html.headers","filename":"fixed-wiz2.html.headers","size":229,"category":"other"},{"path":"bundle/artifacts/fixed-wiz3.html","filename":"fixed-wiz3.html","size":5824,"category":"other"},{"path":"bundle/artifacts/fixed-wiz3.html.headers","filename":"fixed-wiz3.html.headers","size":229,"category":"other"},{"path":"bundle/artifacts/fixed-wiz4.html","filename":"fixed-wiz4.html","size":7568,"category":"other"},{"path":"bundle/artifacts/fixed-wiz4.html.headers","filename":"fixed-wiz4.html.headers","size":229,"category":"other"},{"path":"bundle/artifacts/fixed-wiz5.html","filename":"fixed-wiz5.html","size":6154,"category":"other"},{"path":"bundle/artifacts/fixed-wiz5.html.headers","filename":"fixed-wiz5.html.headers","size":229,"category":"other"},{"path":"bundle/artifacts/fixed-wiz6.html","filename":"fixed-wiz6.html","size":6028,"category":"other"},{"path":"bundle/artifacts/fixed-wiz6.html.headers","filename":"fixed-wiz6.html.headers","size":229,"category":"other"},{"path":"bundle/artifacts/fixed-wiz7.html","filename":"fixed-wiz7.html","size":2629,"category":"other"},{"path":"bundle/artifacts/fixed-wiz7.html.headers","filename":"fixed-wiz7.html.headers","size":418,"category":"other"},{"path":"bundle/artifacts/vuln-proof.headers","filename":"vuln-proof.headers","size":229,"category":"other"},{"path":"bundle/artifacts/vuln-render.headers","filename":"vuln-render.headers","size":229,"category":"other"},{"path":"bundle/artifacts/vuln-version.txt","filename":"vuln-version.txt","size":165,"category":"other"},{"path":"bundle/artifacts/vuln-wiz1.html","filename":"vuln-wiz1.html","size":5851,"category":"other"},{"path":"bundle/artifacts/vuln-wiz1.html.headers","filename":"vuln-wiz1.html.headers","size":229,"category":"other"},{"path":"bundle/artifacts/vuln-wiz2.html","filename":"vuln-wiz2.html","size":5846,"category":"other"},{"path":"bundle/artifacts/vuln-wiz2.html.headers","filename":"vuln-wiz2.html.headers","size":229,"category":"other"},{"path":"bundle/artifacts/vuln-wiz3.html","filename":"vuln-wiz3.html","size":5824,"category":"other"},{"path":"bundle/artifacts/vuln-wiz3.html.headers","filename":"vuln-wiz3.html.headers","size":229,"category":"other"},{"path":"bundle/artifacts/vuln-wiz4.html","filename":"vuln-wiz4.html","size":7568,"category":"other"},{"path":"bundle/artifacts/vuln-wiz4.html.headers","filename":"vuln-wiz4.html.headers","size":229,"category":"other"},{"path":"bundle/artifacts/vuln-wiz5.html","filename":"vuln-wiz5.html","size":6154,"category":"other"},{"path":"bundle/artifacts/vuln-wiz5.html.headers","filename":"vuln-wiz5.html.headers","size":229,"category":"other"},{"path":"bundle/artifacts/vuln-wiz6.html","filename":"vuln-wiz6.html","size":6028,"category":"other"},{"path":"bundle/artifacts/vuln-wiz6.html.headers","filename":"vuln-wiz6.html.headers","size":229,"category":"other"},{"path":"bundle/artifacts/vuln-wiz7.html","filename":"vuln-wiz7.html","size":2629,"category":"other"},{"path":"bundle/artifacts/vuln-wiz7.html.headers","filename":"vuln-wiz7.html.headers","size":416,"category":"other"},{"path":"bundle/repro/derive_attack.sh","filename":"derive_attack.sh","size":7769,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":6655,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":1816,"category":"other"}]}