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 1Leverage CROWD input
Observationlyric-reader.py splits your CROWD input on semicolons and feeds each chunk back through the same instruction parser. Append ;RETURN 0 to any string and the interpreter's line pointer jumps to line 0, where secret_intro lives.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 handler expects free-form text first, so a bare directive is treated as chant text and never reaches the parser. The split only happens when a semicolon is present. The leading semicolon is the whole trick:
;RETURN 0on its own is enough, and any chant text you put in front of it is simply ignored.Tried: Inject
;GOTO 0or;JUMP 0instead of;RETURN 0, assuming the DSL uses a GOTO or JUMP mnemonic for line jumpsThe interpreter knows only its own directives. Grepping the source shows RETURN is the directive that sets the line pointer directly. GOTO and JUMP are not parsed at all, so an injection using them is ignored and execution carries on as normal.
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 2Reveal the secret intro
ObservationOnce the pointer lands on line 0, the interpreter simply carries on from there and prints the assembled flag.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.