Skip to content
0xburger
MY 13:57
All technical notes
POST-EVENT TECHNICAL NOTEweb

V1T CTF 2026

Duck Nettool Revenge

A post-event reconstruction of a severe character filter bypass using shell expansion and a standard-library file reader.

Status
Reconstructed after the event
Published
Station
web

Status of this note

POST-EVENT TECHNICAL NOTE — This is a reconstruction of the challenge method, not a claim that 0xBURGER submitted the solve during the event. The challenge answer, service address, response transcript, and private working artifacts have been removed.

Duck Nettool Revenge wrapped a familiar network utility in a restrictive input filter. The filter looked severe: it admitted digits, punctuation, whitespace, and only one useful letter. The command still ran through a shell, however, so the shell's expansion rules supplied the vocabulary that the filter tried to remove.

The dangerous composition

The core behavior was equivalent to:

command = f"ping -c 1 {target}"
output = subprocess.check_output(
    command,
    shell=True,
    stderr=subprocess.STDOUT,
)

target was checked against an allowlist before interpolation. That check did not make the command safe because shell=True gave the remaining punctuation meaning:

  • ; ended the intended ping command and began another command;
  • / separated path segments;
  • ? matched one character during pathname expansion;
  • spaces separated the expanded words.

An allowlist can restrict characters, but it does not restrict the shell grammar those characters still express.

Turning wildcards into words

The reconstruction used ? patterns with exact segment lengths. If only one installed path matches a pattern, the shell replaces the pattern with that path before execution. A payload can therefore be reasoned about schematically as:

benign-address ; globbed-interpreter globbed-file-reader globbed-source

The one admitted letter was enough to disambiguate the standard-library file-reading module from neighbouring filenames. Segment lengths and the container's small filesystem did the rest. By the time the process launched, the shell had expanded all three wildcard expressions into ordinary executable and file arguments.

This is the key distinction: the application never received a forbidden letter, but the child process still did. Expansion happened after validation and before execution.

Why the file reader mattered

A Python standard-library helper can consume filenames from its arguments and print their lines. Calling that helper through the discovered interpreter avoided the need for filtered command names such as common file-display utilities.

The attack chain was therefore:

  1. submit a harmless address so the intended command begins validly;
  2. terminate it with the allowed command separator;
  3. identify the interpreter through fixed-length wildcard segments;
  4. identify a standard-library file reader using wildcards plus the one available letter;
  5. identify the application source with another unique wildcard pattern;
  6. let shell expansion assemble the forbidden words and print the source.

The event answer appeared in the resulting source response. It is deliberately redacted from this publication.

What to take away

  • Avoid shell=True for user-controlled input. Pass an argument array directly to the process API.
  • Validate the semantic value, such as an IP address or hostname, rather than filtering characters.
  • Run utilities with a minimal environment and filesystem view; uniqueness made the wildcard expansion reliable.
  • Do not place secrets in application source, comments, process arguments, or error responses.
  • Treat wildcard expansion, separators, redirections, substitutions, and environment variables as code execution primitives whenever a shell is involved.

Challenge context: V1T CTF 2026 on CTFtime.