SmartHire 把 MLflow 變初始存取
我拆 SmartHire 的 MLflow 繞過、Pickle RCE 和可寫 plugin 提權,最後整理成可直接抄的攻擊路徑。

我拆 SmartHire 的 MLflow 繞過、Pickle RCE 和可寫 plugin 提權,最後整理成可直接抄的攻擊路徑。
我碰到 SmartHire 這類題目時,第一個感覺通常不是「哇好酷」,而是「這東西怎麼又把幾個本來該分開的信任邊界黏在一起」。表面上它像個 AI 招募平台,前台、模型服務、上傳流程、管理腳本都很完整;實際上你一戳,就會發現它把 public app、ML 服務、序列化模型、以及高權限維運腳本全塞在同一條路徑上。這種架構我看多了,最煩的不是它複雜,是它每一層都只比正常多鬆一點點,剛好鬆到能被串成整條鏈。
我會把這篇當成一份拆解筆記,不是產品介紹。這次的起點是 1337 Sheets 的 SmartHire walkthrough,我從裡面抓出 MLflow 繞過、Pickle RCE、以及 writable plugin 提權這三段,然後把它翻成我自己平常做滲透或 lab 時會用的思路。重點不是背 payload,是看懂這種「看起來很現代,底下卻很老派」的破口怎麼一路接起來。
先別把它當一個站,這其實是兩個信任邊界
訂閱 AI 趨勢週報
每週精選模型發布、工具應用與深度分析,直送信箱。不定期,不騷擾。
不會寄垃圾信,隨時可取消。
“The models.smarthire.htb subdomain was identified as an instance of MLflow, a platform for the machine learning lifecycle.”
翻譯一下就是:前台網站跟模型服務不是同一個安全面。很多人一開始只盯主站,結果在登入頁、表單、CSRF、或前端 JS 上打轉,最後才發現真正的洞在另一個 vhost。SmartHire 的 MLflow 跑在 models.smarthire.htb,這代表你要把它當成獨立系統看,而不是主站的附屬頁面。

我之前也踩過這種坑。表面上是單一產品,實際上後面掛著一個模型管理介面、幾個測試 API、還有一個沒人想碰的 admin host。這種分層如果沒先畫出來,你會一直在錯的地方花力氣。
實操上,我會先做三件事:確認 vhost、確認服務角色、確認哪個 host 才真的做 auth。這裡最常見的錯誤是只做 DNS 枚舉,不做 Host header fuzzing。你如果沒把 sibling host 找出來,後面很多漏洞根本不會出現。
- 先掃 port,再 fuzz Host header。
- 把每個有用的 hostname 都寫進
/etc/hosts。 - 看到「模型」「admin」「internal」這種字眼就別裝沒事,先當成另一個攻擊面。
MLflow 版本老不老,不是資訊,是路線圖
walkthrough 裡面提到這台跑的是 MLflow 2.14.1。這種版本資訊我不會只拿來記錄,因為它其實直接告訴你該查哪一類問題。服務版本一老,通常不是只有一個 CVE,而是 auth、模型載入、artifact 處理、預設設定,整包都可能有歷史包袱。
白話講,看到舊版 MLflow,我不會先想「我要怎麼憑空造 exploit」,我會先想「這版本的認證邊界是不是早就被人研究爛了」。SmartHire 的流程就是這樣:先有 auth bypass 的可能,再去碰後面的 model artifact 與 deserialization。這比硬拗一個新洞實際多了。
我很討厭那種把版本資訊當背景板的寫法。版本不是裝飾品,是你省時間的捷徑。你只要知道服務名跟版本,很多時候就能先把不相關的路徑砍掉。
實操寫法很簡單:先抓 banner、頁尾、錯誤訊息、docker tag、package metadata,任何能露出版本的地方都不要放過。然後分開查三件事:認證、序列化、模型載入。不要只搜「RCE」,那樣通常只會把自己帶進一堆不對的 PoC。
- 版本要查到精確 release,不要只查產品名。
- 先看 auth,再看 artifact,再看 deserialization。
- 如果服務看起來像內網工具,但其實對外開著,優先懷疑預設設定。
CSV 上傳不是資料功能,是測試入口
SmartHire 的前台有 Train Model 和 Make Predictions 這種 CSV 上傳流程。這種功能我看到都會先皺眉,因為它常常是假裝成資料匯入,實際上是在替後端 Python 程式餵受控輸入。CSV 本身不會咬人,但 parser、後處理、以及後面的模型流程會。

這裡最重要的不是「能不能上傳」,而是「上傳後發生什麼事」。如果它只是存檔,那還好;如果它會訓練、載入、轉成模型、再拿去做預測,那你就已經站在 code execution 的門口了。很多 ML app 的問題都不是上傳點本身,而是上傳後那段自動化流程太相信資料格式。
我以前遇過一個內部工具也這樣,前端看起來只是讓人丟 CSV,後端卻把檔案交給一串 Python helper 再丟進序列化格式。團隊一直說「我們有檔案大小限制」,我只能說這種限制對 parser 問題幾乎沒用。
實操上,我會直接做這幾個測試:
- 丟奇怪欄位數、空白列、分隔符混亂的 CSV。
- 看後端有沒有用 Pickle、joblib、yaml、或自製格式。
- 確認上傳結果會不會被另一個服務重用。
Pickle 不是模型格式,是把刀包成資料
walkthrough 最後走到 Pickle deserialization,這一段我完全不意外。Python Pickle 只要碰到不可信輸入,基本上就是在賭對方不會把你送進任意物件重建流程。你不是在「讀資料」,你是在叫 Python 幫你還原物件圖,而那個物件圖如果是你控制的,事情就開始變味。
SmartHire 這裡比較有意思的是,它不是那種一顆 payload 直接打穿的簡單題。作者先碰到 Python 版本、依賴、以及本地模組路徑的問題,後來才把合法的 python_model.pkl 拿來拆。這種過程才像真的攻擊,不是貼一段神奇 payload 就結束。環境不對,payload 常常只會死給你看。
我很在意這種細節,因為它告訴你真正可行的路徑不是「亂塞 shellcode」,而是「先理解 target 預期的 object graph,再把惡意行為塞進同一個結構裡」。walkthrough 裡提到 utils/simplehiringmodel.py 這種本地模組路徑,這就是關鍵:目標不是單純反序列化某個東西,而是會去 import 本地 helper。
實操寫法是:先找合法 artifact,拆它的結構、module 名稱、import path、Python 版本,再決定怎麼重建。不要一開始就寫惡意 payload。先把合法格式摸清楚,很多時候你就會知道該在哪裡插手。
- 先看合法 artifact,不要先做惡意 payload。
- 對齊 Python 版本與依賴。
- 如果有 local module import,就把那條路徑當成攻擊面。
預設帳密很土,但土招通常最省事
這個 MLflow 實例最後還碰到 admin:password。我知道,這種東西看起來很蠢,但我反而覺得它很真實。很多實戰環境不是只有一個漏洞,而是漏洞加上錯誤設定一起把門打開。auth bypass 可能只是讓你進到登入流程,預設帳密則是讓你不用再跟 session、token、或二次驗證糾纏。
我最常看到的問題是,很多人會覺得查預設帳密很 low,於是跳過。結果他們花半天研究一個其實不需要的繞過。我的原則剛好相反:只要服務老、外露、又長得像 copy docs 起來的,就先試預設登入。這不是偷懶,這叫先把最便宜的路走完。
實操上,你應該把預設帳密當成標準檢查項,不要當成菜鳥題。先試產品文件常見的預設值,再看版本對應的常見 setup 組合。如果有 setup wizard 或初始化頁面,更要假設有人可能只做了一半。
- 先試文件預設值,再試版本常見預設值。
- 如果有 setup 流程,確認它是不是半完成狀態。
- 同一個服務如果有多個入口,別假設 auth 一定一致。
可寫的 plugin 目錄,等於把程式碼執行開給你
後半段的提權才是真正讓人翻白眼的地方。walkthrough 說高權限維運腳本會載入一個 Python plugin 目錄,而那個目錄剛好可寫。這種設計我每次看到都想問一句:你們真的有把「可擴充」跟「可被改 code」分清楚嗎?plugin 如果能被低權限使用者寫入,那它就不是功能,它是後門。
白話講,如果 privileged process 會從你能改的路徑載入 Python module,那你根本不需要破密碼或提權漏洞。你只要把會被 import 的檔案換掉,或者新增一個會被吃進去的模組,剩下的交給排程或管理腳本自己跑完。
我以前在內部系統也看過類似狀況,service account 擁有某個 script 目錄的執行權,但檔案權限配得亂七八糟。結果一個本來只是「方便維護」的 plugin 機制,直接變成 root shell。這種事不是理論,是很常見的維運失誤。
實操寫法要盯這幾個點:
- 查
sudo -l、cron、systemd timer、維運腳本。 - 找所有會被 privileged process 讀取的 writable path。
- 特別注意 Python 的 import path 與 plugin config。
我會怎麼把這條鏈固定下來
如果是我自己在打這台,我會把流程壓成很短:先找 split host,再確認 MLflow 版本,接著測 auth 邊界與預設帳密,然後拆合法模型 artifact,最後才去找 writable plugin path。這樣做的好處是,你不會一開始就把時間浪費在亂猜 payload,因為每一步都在縮小空間。
這類題目最討厭的地方,不是它難,而是它很會假裝自己很雜。其實它只是把幾個老問題包進一個看起來很新的殼:vhost 分離、舊版服務、使用者可控上傳、反序列化、以及可寫的程式載入路徑。你只要把這幾個點按順序拆開,鏈就很乾淨。
我也建議你在做筆記時,直接照發現順序寫,不要照攻擊順序亂排。因為讀者真正需要的是你怎麼想到的,不是你最後 shell 拿得多漂亮。
可抄的模板
# SmartHire-style ML app attack checklist(可直接改成你的筆記模板)
## 1) 先切開信任邊界
- 掃 port
- fuzz Host header 找 sibling vhost
- 把有用 hostname 加進 /etc/hosts
- 先分辨 public app / model service / admin service
## 2) 先把 ML 服務版本釘死
- 抓 banner、頁尾、錯誤訊息、docker tag
- 記錄精確版本,不要只寫產品名
- 先查 auth issues,再查 serialization,再查 model loading
- 看是否有預設帳密或半完成 setup
## 3) 把上傳功能當成測試入口
- 找 CSV / JSON / model / archive upload
- 問:parser 是誰?
- 問:上傳後做什麼?train / score / import / deserialize?
- 查有沒有 Pickle、joblib、yaml、或自製 plugin 機制
## 4) 反推合法 artifact 的結構
- 先拆合法檔案,不要先寫惡意 payload
- 記 module 名稱、import path、object graph
- 對齊 Python 版本與依賴
- 如果有 local helper module,就把那條路徑視為攻擊面
## 5) 找提權的 writable code path
- 查 cron、systemd timer、sudo wrapper、維運腳本
- 找 privileged process 會讀的 writable directory
- 特別注意 Python plugin 目錄與 import path
- 能寫就代表能改行為,不只是改資料
## 6) 寫報告時固定順序
- Recon
- vhost discovery
- service fingerprinting
- auth bypass / initial access
- payload construction
- privilege escalation
- lessons learned
## 可重用欄位
- Hostname:
- Port:
- Service:
- Version:
- Auth issue:
- Upload surface:
- Serialization format:
- Writable code path:
- Privilege boundary:
- Final shell path:
這段模板我會留著下次直接改。它不是什麼神秘技巧,但很實用,因為它逼你先把邊界、版本、上傳、序列化、提權這幾層分開看,不會一開始就把自己搞暈。
原始來源是 https://1337sheets.com/hack-the-box-htb-smarthire-writeup-medium-weekly-may-16th-2026/,我這篇是根據那份 walkthrough 做方法論拆解;其中邏輯與模板是我重新整理的,技術脈絡則來自原文與我自己的實戰筆記。