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.
grep -nE 'CROWD|RETURN|secret_intro|split' lyric-reader.pync verbal-sleep.picoctf.net <PORT_FROM_INSTANCE>;RETURN 0Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Leverage CROWD inputObservationI 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 0after any arbitrary string would redirect the interpreter's line pointer (lip) back to line 0 wheresecret_introis defined.When prompted withCrowd:, enter anything followed by;RETURN 0. The interpreter splits on;and runs each chunk as its own instruction, so the injectedRETURN 0snaps the line pointer (lip) back to 0 - the top of the song wheresecret_introlives.bash# At the Crowd: prompt, send literally:bashanything;RETURN 0What didn't work first
Tried: Send just
RETURN 0without a leading payload, expecting the interpreter to jump to line 0The 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 loneRETURN 0is 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 0or;JUMP 0instead of;RETURN 0, assuming the DSL uses a GOTO or JUMP mnemonic for line jumpsThe custom interpreter only recognizes its own defined directives. Running grep on the source reveals that
RETURNis the sole control-flow instruction and it setslipdirectly to the given line number.GOTOandJUMPare not parsed, so the interpreter silently ignores the injected chunk and continues normal execution, never reachingsecret_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, andRETURN, 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 andGOTOstatements.The vulnerability is command injection through a delimiter. The script trusts user input from
CROWDprompts 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 0directive sets the instruction pointer back to line 0 - the beginning of the file - where the hiddensecret_introsection 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.Step 2
Reveal the secret introObservationI noticed thatRETURN 0setslipto line 0 where the hiddensecret_introblock 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 printssecret_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
RETURNinstruction. 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.