Poem CTF Solution, the PHP Filter Way
DarkForge Labs’ First CTF
First off, huge congratulations to everyone who completed the challenge — and a special shoutout to et3rnos, the first to solve it and our official winner!
This CTF was inspired by the article Impossible XXE in PHP by Aleksandr Zhurnakov. If you haven’t read it yet, you’re missing out.
The Challenge

The challenge was hosted at https://ctf.darkfor.ge with the source available on our GitHub.
Digging into index.php, we find three interesting things:
- The file parses a user-controlled parameter using:
parse_ini_file($poem_file, true); - The
$poem_filevariable is controllable via thepoem_fileGET parameter:
$poem_file = $_GET['poem_file'] ?? '/var/www/html/poem.ini'; - The INI key it looks for is
POEM:
$config['POEM']
(Note: INI keys are case sensitive.)
From the Dockerfile, we learn the flag is stored at /var/secret/flag.txt.
Solution
Your first instinct might be to try this:
https://ctf.darkfor.ge/?poem_file=/var/secret/flag.txt
But that won’t work. The flag file doesn’t contain INI-formatted content, and parse_ini_file() expects a valid format like:
POEM = FLAG_VALUE
So, the trick is to prepend the necessary INI key to the flag content.
Dead Ends
If you’re familiar with the modern wrapwrap technique by Charles Fol, you might try using that. However, due to server constraints on URL length, you’ll hit a “Requested URI Too Long” error.

Filters to the Rescue
It all starts with this legendary post by Gynvael in 2018:
Surprising CTF task solution using php://filter
Shortly after this post, lolknop took it further with a custom payload generator you can find in this Gist.
This is perfect — we can use it to prepend the line POEM = to the flag content, making it parsable by parse_ini_file().
But… there’s a slight snag. Both POEM = and its base64 form UE9FTSA9 contain characters not supported by lolknop’s script. The workaround? Use a tab and remove the space before the =:
\tPOEM=
Which base64-encodes to: CVBPRU09 — a string with characters fully supported by the Gist tooling. Excellent.
This allows us to build a monster filter chain like this:
php://filter/.../resource=/var/secret/flag.txt
(Note: Click the link for the full URL)
Easier Payload Generation
For a much cleaner experience, use the PHP Filter Chain Generator by Rémi Matasse of Synacktiv. It already maps most ASCII character to their supported encodings.
Just run:
python php_filter_chain_generator.py --chain 'POEM ='
Then replace resource= with your target file (/var/secret/flag.txt), and voilà, payload done.
(Note: Click the link for the full URL)

For deeper understanding, I highly recommend reading their full post:
PHP Filters Chain: What is it and how to use it
You can also explore PHP_INCLUDE_TO_SHELL_CHAR_DICT by Wupco for fuzzing and payload crafting.
Final Thoughts
This challenge was all about exploring edge-case behavior in PHP’s obscure corners and the community’s creativity in weaponizing those quirks. The linked articles are truly mind-bending and absolutely worth a read.
Big thanks to everyone who played, and stay tuned, the DarkForge team has a lot more coming your way.