GHSA-9369-69wj-7m2f
YesWiki Vulnerable to Authenticated PHP Object Injection in BazarImportAction via unserialize
Details
## Details
### Sink
`tools/bazar/services/CSVManager.php` line 372-399:
``` public function importEntry(array $importedEntries, string $formId): ?array { if (!$this->importdone) { // ... foreach ($importedEntries as $entry) { $entry = unserialize(base64_decode($entry)); // <-- SINK $entry = array_map('strval', $entry); // ... ```
There 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.
### Source
`tools/bazar/actions/BazarImportAction.php`:
``` // formatArguments() 'mode' => (isset($_POST['submit_file']) && !empty($_FILES['fileimport']['name'])) ? 'submitfile' : (isset($_POST['importfiche']) ? 'importentries' : 'default'), 'importentries' => $_POST['importfiche'] ?? null,
// run() case 'importentries': // ... $importedEntries = $this->CSVManager->importEntry($this->arguments['importentries'], $vID['id']); break; ```
`$_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>`.
### Reachability
1. 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.
2. `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.
3. 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.
4. Therefore the full kill chain for a remote attacker is:
a. Identify any admin user on the target wiki. 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`. c. The admin's session cookie is sent automatically; the action passes `checkSecuredACL`; the unserialize fires.
### Gadget chain availability
`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.
This 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.
### Past advisories cross-check
YesWiki'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.
## PoC
A 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.
Run:
``` php poc.php ```
Output (verbatim):
``` Crafted importfiche[0] payload (form-ready, urlencoded): YToxOntpOjA7Tzo2OiJHYWRnZXQiOjE6e3M6NjoibWFya2VyIjtzOjIyOiJQV05FRC1GUk9NLVVOU0VSSUFMSVpFIjt9fQ%3D%3D
== before importEntry == [Gadget] __destruct fired with marker='PWNED-FROM-UNSERIALIZE' PHP Fatal error: Uncaught Error: Object of class Gadget could not be converted to string ... [Gadget] __destruct fired with marker='PWNED-FROM-UNSERIALIZE' ```
The 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.
End-to-end against a live YesWiki install:
``` curl -i -b "yeswiki_session=<admin_cookie>" \ -X POST "https://wiki.example.com/?BazaR&vue=importer&id_typeannonce=1" \ --data-urlencode \ "importfiche[0]=YToxOntpOjA7Tzo2OiJHYWRnZXQiOjE6e3M6NjoibWFya2VyIjtzOjIyOiJQV05FRC1GUk9NLVVOU0VSSUFMSVpFIjt9fQ==" ```
(replace the payload with a real PHPGGC `Doctrine/FW1` or `Doctrine/RCE1` output to obtain RCE on the target host).
## Impact
- 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). - An attacker who has already compromised an admin password upgrades from "wiki content management" to "OS shell on the hosting box". - 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.
## Suggested fix
1. `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. 2. `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.
Are you affected?
Enter the version of the package you're using.
Affected packages
0 Fixed in: 4.6.6 composer require yeswiki/yeswiki:^4.6.6