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
Walk me through it- Step 1Leverage CROWD inputWhen prompted with
Crowd:, 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 0Learn 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 introJumping 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
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.
Flag
picoCTF{70637h3r_f0r3v3r_750...}
Any input containing `;RETURN 0` works because the preceding characters are ignored.