# CVE-2026-124020 / CVE-2026-124021 — Control Web Panel password-recovery takeover + API authz bypass
 before `webpanel_version 1.30.6` ships two critical flaws in its password-recovery and remote-API code, both fixed in 1.30.6 and both credited to Valentin Lobstein (Chocapikk) via VulnCheck coordinated disclosure (2026-11-13). CVE-2026-124020 (CWE-640) injects the HTTP `Host` header into the `/v1/lostpass/` route's reset-URL generation, so the recovery mail carries an attacker-hosted link; when an automated mail-security scanner clicks that link, the live recovery token leaks to the attacker and is replayed at the panel's reset page to set a new password and take the account over, with the File-Manager and Crontab modules as the command-execution payoff. CVE-2026-124021 (CWE-863) is an authorization bypass in the `validate()` function of `api/v1/index.php` (the external API on port 2304): a holder of a password-recovery token plus a predictable legacy signature obtains valid remote-API authorization without an API key, reaching `POST /cronjobsusers/` (arbitrary cron commands, RCE as the hosting account) and the global account-list and account-detail routes, which expose usernames, emails and password hashes. CWP is a widely deployed hosting panel with a 2025 RCE track record (CVE-2025-48703, KEV-listed and mass-exploited), so recovery-flow and API-gate flaws in the same code family matter well beyond their scores.
3. The leaked token is replayed: `GET /login/?acc=reconfir&idsession=` renders the set-new-password form.
4. `POST /login/index.php?acc=newpass` with the token sets an attacker-chosen password.
5. The panel issues a user session for the attacker-chosen password — account takeover.
**The poisoned-link step itself did NOT reproduce in this lab** (`evidence/05-host-injection-probe-matrix.txt`): on every reachable path (the root-executing `/v1/lostpass/` route and the panel's `acc=lostpass` flow) and every header variant (plain `Host`, `Host` without port, `X-Forwarded-Host`), the 1.30.5 build in this lab generated the reset URL from its configured panel URL, never from the injected header; two same-second resets with different `Host` values produced different tokens and an offline over hash-formula candidates found no `Host`-derived token. The advisory's chain therefore stands on the vendor's disclosure for its poisoning step, while the token-issuance → replay → takeover half is verified end-to-end here. The CVSS `AT:P` (attack requirement present) is the scanner-click precondition and is stated plainly: without a scanner or user clicking the poisoned link, no token leaves the server.
**CVE-2026-124021 — validate() bypass: NOT REPRODUCED** (`evidence/04-api-bypass-probes-both.txt`): on the 1.30.5 lab box the 2304 gate accepts a real API Manager key (`{"status":"Error","result":"There are no scheduled tasks"}` for `cronjobsusers`), requires a key otherwise (`403 API key required`), and answers `key does not exist` for both random keys and the live recovery token across ~30 legacy-signature candidates (md5/sha1/sha256 over token, token+user, token+action, with `token`/`signature`/`client_id` parameter shapes). One observed asymmetry: with a `user` parameter present, a random key is refused earlier (`403 API key required`) than the recovery token (`200 key does not exist`), which shows the recovery token is handled by a different validation branch — but no signature formula was recovered, so no cron RCE or password-hash disclosure is demonstrated here. `poc.py --mode cron` is kept as the probe harness with honest verdicts.
docker compose up -d # vulnerable 1.30.5 + fixed 1.30.6, plus the attacker SMTP/exfil hook
# wait for the CWP installer inside each container (15-30 min, watch cwp-install.log),
# then seed an account and an API key in each panel (API Manager -> add key)
python3 poc.py --mode takeover --host 127.0.0.1 --panel-port 2083 --api-port 2304 \
--user webhost --email [email protected] --token
python3 poc.py --mode cron --host 127.0.0.1 --api-port 2304 --user webhost --token
Flags: `--host` target (default `127.0.0.1`), `--panel-port` user-panel TLS port (2083), `--api-port` external API port (2304), `--user/--email` targeted account, `--token` live recovery token (`idsession=` value from the reset mail), `--poison-host` attacker `Host` header, `--password` password to set. `--mode cron` exit codes: 0 = bypass verified, 2 = not reproduced (control behavior).
Vulnerable 1.30.5 (`evidence/01-takeover-chain-vuln-1.30.5.txt`, abridged):
=== [2] the reset mail, captured on the lab SMTP relay ===
Subject: =?UTF-8?B?Q2hhbmdlIGFjY291bnQgcGFzc3dvcmQgd2ViaG9zdA==?=
leaked recovery token: 8a1803935bfc1987675c21b3c1101a2a
=== [3] replay: the reset page accepts the token ===
GET -> 200
=== [5] takeover proof: panel login with the attacker-chosen password ===
POST -> 200
user-panel session cookie issued: 1 (1 = yes)
The recovery token is a 32-hex `idsession` stored in `oauthv2.oauth_authorization_codes` with the account name as `client_id` — the same table the external API's gate consults, which is what ties the two CVEs together in one lab. `evidence/02` shows the same probes against the fixed 1.30.6 box and `evidence/04` the gate responses on both.
- The password-recovery flow splits across two services: the user-panel login dispatcher (unprivileged pool) and a root-executing `/v1/` API dispatcher (`/usr/local/cwpsrv/var/services/user_api/v1/index.php`, reached via `/v1/lostpass/`). The advisory's fix lands in this family (the 1.30.6 point release changes `admin/login/index.php`, `resources/admin/include/libs/cwp_login_token.php` and the login-security modules), and the recovery token is persisted into `oauthv2.oauth_authorization_codes` (`authorization_code`, `client_id`, base64-packed `redirect_uri` with the requester IP and timestamps) — the exact table named by the sibling `oauth-authorization-codes` advisory.
- The token is replayable at the public panel: `acc=reconfir&idsession=` renders the reset form and `acc=newpass` consumes it without requiring the mail or the original password, so a single leaked hex string is full account takeover; the File-Manager and Crontab modules then execute as the hosting account.
- The external API on 2304 (`/v1/cronjobsusers/`, `action=add` with `user`, `execute`, `command`) runs cron additions per account; its `validate()` is the gate CVE-2026-124021 describes — the advisory pairs it with the `oauth_authorization_codes` store, and 1.30.1's changelog ("every API request now requires an API key", with a documented `api_strict_guard_off` compatibility switch) shows the legacy acceptance path that the bypass walks.
- Why the lab could not complete the poisoning step: the reset-URL host was config-derived on every reachable path of this build (see `evidence/05`); reproducing it likely requires the exact request shape the finder used (the CWP code is ionCube-encoded, which blocks source-level confirmation here).
- Mitigation: upgrade to 1.30.6; then disable password recovery where not needed (or reset all outstanding recovery tokens — each is a live `oauth_authorization_codes` row for ~2h), block `Host`-header overrides at the proxy (pin the panel URL), restrict port 2304 to integration sources, and audit crontabs of all hosting accounts for unexpected jobs.
- Detection: password-reset mails whose link host differs from the panel's own hostname; `oauthv2.oauth_authorization_codes` rows whose packed `redirect_uri` requester IP is not the panel; unexpected `POST /v1/lostpass/` and `acc=lostpass|reconfir|newpass` traffic; cron jobs added through the 2304 API without a matching API-key audit line.
- Lab quirks worth knowing: CWP's container install needs the CDN hostname pinned in `/etc/hosts` (postfix's chroot needs the mirror copy), the installer-generated `resources/admin/include/postfix.php` must be restored after any panel-code swap, and the recovery mail relay is the honest exfil-channel analog — point postfix `relayhost` at your catcher and read the token from the mail, exactly as a scanner would from the poisoned link.
- Freshness 2026-11-14 (re-checked at packaging): nomi-sec PoC-in-GitHub has no JSON entries for either CVE (404), GitHub repo is empty for both IDs, GitHub code returns only cvelistV5 mirrors and the fankh/vulnerability-poc AI-scanner stubs (the formulaic non-exploit pattern), Exploit-DB has none. The gap is still open.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
