Flag Hunters picoCTF 2025 Solution

Published: April 2, 2025

Description

lyric-reader.py prints verses and a refrain but never displays the secret intro that holds the flag. Use the CROWD prompt to inject a RETURN 0 directive and jump to the hidden lines.

Read lyric-reader.py to find that CROWD-prompt input is split on ; and re-fed into the same instruction parser. That is your injection point.

Confirm that the script defines a secret_intro block at line 0 and that normal control flow skips it (so you need to jump there).

Connect to the remote service (or run the script locally) and wait for the first CROWD prompt.

bash
grep -nE 'CROWD|RETURN|secret_intro|split' lyric-reader.py
bash
nc verbal-sleep.picoctf.net <PORT_FROM_INSTANCE>
bash
;RETURN 0

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
The same data-parsed-as-code root cause shows up in real apps; see the Command Injection for CTF guide for shell-level versions of this trick.
  1. Step 1
    Leverage CROWD input
    Observation
    I noticed that lyric-reader.py splits CROWD prompt input on ; and re-feeds each chunk into the same instruction parser, which suggested that injecting ;RETURN 0 after any arbitrary string would redirect the interpreter's line pointer (lip) back to line 0 where secret_intro is defined.
    When prompted with Crowd:, enter anything followed by ;RETURN 0. The interpreter splits on ; and runs each chunk as its own instruction, so the injected RETURN 0 snaps the line pointer (lip) back to 0 - the top of the song where secret_intro lives.
    bash
    # At the Crowd: prompt, send literally:
    bash
    anything;RETURN 0
    What didn't work first

    Tried: Send just RETURN 0 without a leading payload, expecting the interpreter to jump to line 0

    The CROWD handler strips or rejects bare directive keywords entered as the crowd chant, because it expects free-form text first. The split on ; only fires when a semicolon is present, so a lone RETURN 0 is treated as literal crowd chant text and never reaches the instruction parser. Prepending any non-empty string before the semicolon (e.g. anything;RETURN 0) satisfies the parser's expectation for crowd text while still injecting the directive.

    Tried: Inject ;GOTO 0 or ;JUMP 0 instead of ;RETURN 0, assuming the DSL uses a GOTO or JUMP mnemonic for line jumps

    The custom interpreter only recognizes its own defined directives. Running grep on the source reveals that RETURN is the sole control-flow instruction and it sets lip directly to the given line number. GOTO and JUMP are not parsed, so the interpreter silently ignores the injected chunk and continues normal execution, never reaching secret_intro.

    Learn more

    This challenge implements a custom domain-specific language (DSL) - a mini interpreter that processes song lyrics as a script. The interpreter reads lines sequentially, handles special directives like VERSE, REFRAIN, CROWD, and RETURN, and maintains an instruction pointer (lip) that tracks the current line. This architecture is similar in concept to early programming languages like BASIC, which also used line numbers and GOTO statements.

    The vulnerability is command injection through a delimiter. The script trusts user input from CROWD prompts but then processes it through the same parser as the script itself, splitting on semicolons. By injecting a semicolon followed by a valid directive, the attacker forces the interpreter to execute arbitrary instructions mid-stream. This is conceptually identical to SQL injection (where user input is interpolated into a SQL statement) and shell injection (where user input is passed to a shell that interprets semicolons as command separators).

    The RETURN 0 directive sets the instruction pointer back to line 0 - the beginning of the file - where the hidden secret_intro section is defined. Because normal execution never reaches line 0 (it starts after the intro), the flag is never printed under normal circumstances. This "dead code" pattern is sometimes used in CTFs to hide secrets that require control-flow manipulation to reach.

  2. Step 2
    Reveal the secret intro
    Observation
    I noticed that RETURN 0 sets lip to line 0 where the hidden secret_intro block lives, which suggested that once the injection redirected execution there, the interpreter would continue running and print the concatenated flag automatically.
    Jumping to line 0 prints secret_intro, which concatenates the flag. Let the script continue and read the picoCTF string.
    Learn more

    Arbitrary control flow - the ability to redirect a program's execution pointer to any location - is one of the most powerful primitives in binary exploitation. In this scripted context, it is achieved simply by injecting a RETURN instruction. In binary exploitation, equivalent capabilities require stack-based buffer overflows, return-oriented programming (ROP), or use-after-free vulnerabilities to overwrite the instruction pointer.

    The principle of least privilege suggests that user-provided data should never be treated as code. The fix for this script would be to either sanitize CROWD input (removing semicolons and rejecting directive keywords) or process CROWD input in a completely separate context that cannot influence the interpreter's control flow. This separation between data and code is a fundamental principle in secure programming.

    In real-world applications, similar vulnerabilities appear in template injection (user input rendered as a template that executes code), macro injection (user content treated as spreadsheet formulas), and eval-based architectures (user strings passed to eval or exec). All share the same root cause: insufficient separation between data and execution context.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.

Flag

Reveal flag

picoCTF{70637h3r_f0r3v3r_750...}

Any input containing `;RETURN 0` works because the preceding characters are ignored.

Key takeaway

Injection vulnerabilities arise whenever user-supplied input is parsed by the same engine that processes trusted program logic, with no separation between data and code. A semicolon delimiter here plays the same role as a quote character in SQL or a newline in an SMTP header: it breaks out of the data context and opens a code context. The defense is always the same principle, applied at the parser level: treat user input as opaque data and never allow it to influence the interpreter's control flow or instruction stream.

Related reading

Want more picoCTF 2025 writeups?

Tools used in this challenge

What to try next