{"repro_id":"REPRO-2026-00256","version":30,"title":"Pagekit CMS privilege escalation leading to RCE","repro_type":"security","status":"published","severity":"high","description":"Pagekit CMS 1.0.18 contains a privilege escalation vulnerability that allows authenticated users to escalate privileges and execute remote code. Reproduce: deploy Pagekit 1.0.18, create a low-privileged authenticated user, exploit the privilege escalation to become admin, and then use the admin functionality to execute code.","root_cause":"# RCA Report — CVE-2026-57518\n\n## Summary\n\nPagekit CMS 1.0.18 contains a privilege escalation vulnerability in\n`UserApiController::saveAction()` that allows authenticated users holding the\n`user: manage users` permission to self-assign arbitrary custom roles. The\nauthorization check only prevents assignment of the built-in Administrator role\n(hard-coded ID=3); any custom role (ID ≥ 4) — including one with elevated\npermissions such as `system: manage packages` — passes through unchecked. An\nattacker can exploit this to gain the `system: manage packages` permission,\nthen upload and install a malicious PHP extension package containing a webshell\nthrough the admin package installer, achieving remote code execution on the\nserver.\n\n## Impact\n\n- **Package/component affected:** `pagekit/pagekit` —\n  `app/system/modules/user/src/Controller/UserApiController.php`\n  (`saveAction`, lines 178–183)\n- **Affected versions:** Pagekit CMS ≤ 1.0.18 (project archived 2023-12-01;\n  no patched release exists)\n- **Risk level:** High (CVSS 8.8 — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)\n- **Consequences:** Full server compromise. An authenticated low-privileged\n  user with `user: manage users` can escalate to arbitrary code execution as\n  the web server user (`www-data`).\n\n## Impact Parity\n\n- **Disclosed/claimed maximum impact:** Privilege escalation leading to remote\n  code execution (code_execution)\n- **Reproduced impact from this run:** Full privilege escalation to\n  `system: manage packages`, followed by upload and installation of a malicious\n  PHP package, followed by confirmed remote code execution (`uid=33(www-data)`)\n- **Parity:** `full`\n- **Not demonstrated:** N/A — the full chain from low-privilege user to\n  arbitrary command execution was demonstrated end-to-end against the real\n  running Pagekit service via its HTTP API.\n\n## Root Cause\n\nThe vulnerability is in `UserApiController::saveAction()` at\n`app/system/modules/user/src/Controller/UserApiController.php`, lines 178–183:\n\n```php\n$key    = array_search(Role::ROLE_ADMINISTRATOR, @$data['roles'] ?: []);\n$add    = false !== $key && !$user->isAdministrator();\n$remove = false === $key && $user->isAdministrator();\n\nif (($self && $remove) || !App::user()->isAdministrator() && ($remove || $add)) {\n    App::abort(403, 'Cannot add/remove Admin Role.');\n}\n```\n\n`Role::ROLE_ADMINISTRATOR` is a constant equal to `3` (defined in\n`app/system/modules/user/src/Model/Role.php`). The authorization check\n**only** tests whether the Administrator role (ID=3) is being added or\nremoved. Custom roles created by an administrator (which receive IDs 4, 5, 6,\n…) are never subjected to any authorization check. Consequently, any user who\nhas the `user: manage users` permission (required by the `@Access(\"user:\nmanage users\")` annotation on `UserApiController`) can call\n`POST /api/user/{own_id}` with a `roles` array that includes a custom role\nwith elevated permissions — for example, `system: manage packages` — and\nthe server will silently accept and persist the change.\n\nOnce the attacker holds `system: manage packages` (and `system: access admin\narea`), the admin package installer at `POST /admin/system/package/upload`\nand `POST /admin/system/package/install` becomes available. The attacker\nuploads a ZIP archive containing a `composer.json` (type\n`pagekit-extension`) and a PHP webshell (`index.php`). The installer extracts\nthe package into the web-accessible `packages/` directory, and the webshell\nis immediately reachable at\n`/packages/pagekit/shell/index.php?cmd=<command>`, executing arbitrary\ncommands as the web server user.\n\n**Fix commit:** None — the Pagekit project was archived on 2023-12-01 and no\npatched version has been released.\n\n## Reproduction Steps\n\n1. **Script:** `bundle/repro/reproduction_steps.sh`\n2. **What the script does:**\n   - Downloads (or reuses a cached copy of) the official Pagekit 1.0.18\n     release ZIP (which includes all PHP dependencies in `app/vendor/`).\n   - Builds a Docker image based on `php:7.4-apache` with the required PHP\n     extensions (gd, zip, pdo_sqlite, opcache) and deploys Pagekit with\n     Apache + mod_rewrite.\n   - Starts the container and installs Pagekit via its installer API\n     (`POST /installer/install`) using a SQLite database, creating an\n     administrator account.\n   - As administrator, creates two custom roles:\n     - **Package Manager** (ID 4): `system: manage packages` +\n       `system: access admin area`\n     - **User Manager** (ID 5): `user: manage users`\n   - As administrator, creates a low-privileged **editor** user (ID 2) with\n     the User Manager role.\n   - As editor, calls `POST /api/user/2` with `{\"user\":{\"id\":2,\"roles\":[2,4]}}`\n     — self-assigning the Package Manager role. The `saveAction` only blocks\n     role ID 3 (Administrator); the custom role ID 4 passes through unchecked.\n   - As the now-escalated editor, uploads a malicious extension ZIP\n     (containing a PHP webshell) via `POST /admin/system/package/upload` and\n     installs it via `POST /admin/system/package/install`.\n   - Accesses `/packages/pagekit/shell/index.php?cmd=id` and verifies the\n     output contains `uid=`, confirming remote code execution.\n3. **Expected evidence of reproduction:**\n   - `bundle/logs/reproduction_steps.log` — full execution log\n   - `bundle/logs/exploit_evidence.txt` — structured evidence summary\n   - `bundle/logs/rce_id_output.txt` — output of `id` command via webshell\n     (`uid=33(www-data) gid=33(www-data) groups=33(www-data)`)\n   - `bundle/logs/rce_whoami_output.txt` — output of `whoami` (`www-data`)\n   - `bundle/repro/runtime_manifest.json` — runtime evidence manifest\n\n## Evidence\n\n### Key excerpts\n\n**Privilege escalation (Step 3):**\n```\nEditor BEFORE escalation: {\"id\":2,...,\"roles\":[2,5],...}\nEscalation response: {\"message\":\"success\",\"user\":{\"id\":2,...,\"roles\":[2,4],...}}\nPRIVILEGE ESCALATION CONFIRMED — editor now has 'system: manage packages'\n```\n\n**RCE (Step 6):**\n```\nRCE output (id): uid=33(www-data) gid=33(www-data) groups=33(www-data)\nRCE output (whoami): www-data\n=== RCE CONFIRMED ===\n```\n\n### Environment details\n\n- **OS:** Ubuntu 26.04 LTS (host); Debian Bullseye (container)\n- **PHP:** 7.4.33 (Apache, inside Docker container)\n- **Database:** SQLite (`pagekit.db`)\n- **Pagekit:** 1.0.18 (official release from GitHub)\n- **Web server:** Apache 2.4.54 with mod_rewrite\n\n### Sandbox adaptations (non-security-relevant)\n\n1. **Docker networking:** The sandbox restricts host-to-container port\n   forwarding, so all HTTP requests to the running Pagekit service are made\n   via `docker exec` (curl inside the container). This does not alter the\n   product's request handling — the same HTTP requests reach the same Apache\n   + PHP + Pagekit stack.\n2. **Session bootstrap:** A PHP helper (`create_session.php`) creates a valid\n   authenticated session directly in the database (session + auth tables) to\n   bypass a PHP 7.4 / Symfony 3.0 session-storage interaction issue that\n   prevents normal web login via curl. The auth state created is identical to\n   a successful web login (same cookies, same database records, same CSRF\n   token derivation).\n3. **Composer marketplace repository:** The package installer's Composer\n   blueprint includes a remote `composer` repository pointing to\n   `https://pagekit.com`. The sandbox has no outbound network, so this\n   repository entry is removed from the blueprint, leaving only the local\n   `artifact` repository. This does not change any security-relevant code\n   path — it only avoids a network fetch for the marketplace index that is\n   irrelevant to the exploit.\n\n## Recommendations / Next Steps\n\n### Suggested fix\n\nThe authorization check in `saveAction` should validate **all** role\nassignments, not just the Administrator role. A correct implementation would\nverify that the calling user is permitted to assign each role in the\n`$data['roles']` array — for example, by checking that the calling user\neither is an administrator or holds a permission that authorizes assigning\neach specific role. At minimum, the check should prevent any non-administrator\nfrom assigning roles with `trusted` permissions (such as\n`system: manage packages`) to themselves.\n\n### Upgrade guidance\n\nPagekit is archived and unmaintained. Users should migrate to an actively\nmaintained CMS. If migration is not immediately feasible, a mitigation is to\nnever grant the `user: manage users` permission to non-administrator accounts,\nand to avoid creating custom roles with `trusted` permissions.\n\n### Testing recommendations\n\n- Add a unit test that verifies a non-administrator user with\n  `user: manage users` cannot self-assign a custom role with `trusted`\n  permissions.\n- Add an integration test that verifies the package installer rejects uploads\n  from users without `system: manage packages`.\n\n## Additional Notes\n\n### Idempotency confirmation\n\nThe script was run twice consecutively, both times producing the same result:\nprivilege escalation confirmed and RCE confirmed (`uid=33(www-data)`). Each\nrun creates a fresh Docker container with a clean Pagekit installation,\nensuring no state carries over between runs.\n\n### Limitations\n\n- The project is archived; no fixed version exists for a negative control.\n- The webshell executes as `www-data` (the Apache user), not as root. Full\n  root compromise would require a separate local privilege escalation,\n  which is out of scope for this CVE.\n- The `user: manage users` precondition is realistic: Pagekit supports\n  delegating user management to non-owner accounts (e.g., a \"User Manager\"\n  or \"Editor\" role), which is the exact scenario this vulnerability targets.\n","cve_id":"CVE-2026-57518","cwe_id":"CWE-862 Missing Authorization / CWE-269 Improper Privilege Management","source_url":"https://github.com/pagekit/pagekit","package":{"name":"pagekit/pagekit","ecosystem":"github","affected_versions":"<= 1.0.18","fixed_version":"None available; project archived"},"reproduced_at":"2026-07-06T09:34:54.172511+00:00","duration_secs":1395.0,"tool_calls":291,"handoffs":2,"total_cost_usd":6.595436059999998,"agent_costs":{"judge":0.00703055,"repro":4.850436479999998,"support":0.08918232,"vuln_variant":1.6487867100000002},"cost_breakdown":{"judge":{"gpt-5.4-mini":0.00703055},"repro":{"accounts/fireworks/routers/glm-5p2-fast":4.850436479999998},"support":{"accounts/fireworks/routers/glm-5p2-fast":0.08918232},"vuln_variant":{"accounts/fireworks/routers/glm-5p2-fast":1.6487867100000002}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-07-06T09:38:11.641699+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":20608,"category":"reproduction_script"},{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":9688,"category":"analysis"},{"path":"bundle/vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":26900,"category":"reproduction_script"},{"path":"bundle/vuln_variant/rca_report.md","filename":"rca_report.md","size":14583,"category":"analysis"},{"path":"bundle/artifact_promotion_manifest.json","filename":"artifact_promotion_manifest.json","size":7444,"category":"other"},{"path":"bundle/vuln_variant/source_identity.json","filename":"source_identity.json","size":1919,"category":"other"},{"path":"bundle/vuln_variant/root_cause_equivalence.json","filename":"root_cause_equivalence.json","size":4683,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":932,"category":"other"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":744,"category":"other"},{"path":"bundle/logs/rce_id_output.txt","filename":"rce_id_output.txt","size":54,"category":"other"},{"path":"bundle/logs/exploit_evidence.txt","filename":"exploit_evidence.txt","size":397,"category":"other"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":3711,"category":"log"},{"path":"bundle/logs/rce_whoami_output.txt","filename":"rce_whoami_output.txt","size":9,"category":"other"},{"path":"bundle/logs/vuln_variant/rce_id_output.txt","filename":"rce_id_output.txt","size":54,"category":"other"},{"path":"bundle/logs/vuln_variant/exploit_evidence.txt","filename":"exploit_evidence.txt","size":1030,"category":"other"},{"path":"bundle/logs/vuln_variant/reproduction_steps.log","filename":"reproduction_steps.log","size":4455,"category":"log"},{"path":"bundle/vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":4270,"category":"other"},{"path":"bundle/vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":3306,"category":"other"},{"path":"bundle/vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":8803,"category":"documentation"},{"path":"bundle/vuln_variant/runtime_manifest.json","filename":"runtime_manifest.json","size":965,"category":"other"},{"path":"bundle/logs/vuln_variant/rce_whoami_output.txt","filename":"rce_whoami_output.txt","size":9,"category":"other"},{"path":"bundle/logs/vuln_variant/fixed_version.txt","filename":"fixed_version.txt","size":74,"category":"other"}]}