PHP quirks/security/CTF tricks/tips (last update: 2018-06-18, created: 2012-09-20) back to the list ↑
Things I found strange or/and interesting in PHP. Thankfully most of them can be found in the official docs, if one actually cares to read it.

PHP extensions to try

Various default HTTP server settings allow different file extensions to be interpreted as PHP (assuming the files are mapped over HTTP).

(Arch, all versions) php$: .php
(CentOS with PHP 5.4) php$: .php
(Fedora with PHP 7.0, 7.1) php$: .php
(Fedora with PHP 7.2) (php|phar)$: .php, .phar
(Ubuntu/Debian with PHP 7.1, 7.2) ph(ar|p|tml): .phar, .php, .phtml
(Ubuntu/Debian with PHP 7.0) ph(p[3457]?|t|tml): .php, .php3, .php4, .php5, .php7, .pht, .phtml
(Debian with PHP 5.6) ph(p[345]?|t|tml): .php, .php3, .php4, .php5, .pht, .phtml
(Ubuntu with PHP 5.5) ph(p[345]?|t|tml): .php, .php3, .php4, .php5, .pht, .phtml
(Debian with PHP 5.4) ph(p[345]?|t|tml): .php, .php3, .php4, .php5, .pht, .phtml
(Ubuntu with PHP 5.3) ph(p3?|tml): .php, .php3, .phtml
(Ubuntu with PHP 5.?): .phtm (source)

Multiple extensions
As mentioned in the Apache 2 AddHandler documentation [1], a file can have multiple extensions [2] - in this case the MIME-type mapping done using AddHandler (vs FilesMatch+SetHandler) will be matched against each of these extensions. Given this, a file name file.php.asdf will still be executed as a PHP script.
[1] AddHandler Directive
[2] Files with Multiple Extensions

This behavior can be observed in:
(CentOS 6.9 with PHP 5.3): .php
(maybe others?)

Return value of include / include_once

In manual: http://php.net/manual/en/function.include.php

Include can fail (require can fail too, but it die()'s on fail) and that's good, especially in case you want some modules sometimes available and sometimes missing. In case it fails, it returns False.
However, since include() is not a function, but a directive, it doesn't need it's arguments to be in brackets - everything after include is it's argument (until the end of line or logical block). So, this won't work:

if(include("x.php") === False) handle_problem();
Since the argument of the include directive is, as said, everything up until the end of logical block (that would be the closing bracket of if). So the argument is ("x.php") === False, which evaluates to False. So basically this is just if(include False) handle_problem();, which is probably not the thing one has in mind.

Proper solution is to enclose include + it's argument in another set of brackets, e.g.:

if((include "x.php") === False) handle_problem();

In the end, it's kinda similar (but different) to C/C++ preprocessor and #define ADD(a,b) a+b instead of #define ADD(a,b) ((a)+(b)) - i.e. you never know what happens to the argument.

Conditional function declaration

The function function_exists does just what the name says. PHP also allows to dynamically (or rather: conditionally) declare a function at runtime, depending e.g. on the fact that that function doesn't exist.

Example code that doesn't work:

if(!function_exists("my_func")) {
  // Register a replacement function.
  function my_func() {
    echo "Hi, I am a replacement function.\n";
  }
}

The problem here is that PHP parser will find the function definition even if it's in an if statement, so this isn't really conditional.
However (as explained in the linked comment), one can place the function definition in a function definition (this is a good place for a "yo dawg, we heard you like functions, so..."), which will work as intended - i.e. the function "new" function will be defined only after calling the function it's contained within. E.g.:

function register_my_func() {
  function my_func() {
    echo "Hi, I am a replacement function.\n";
  }  
}

if(!function_exists("my_func")) {
  // Register a replacement function.
  register_my_func();
}

My vote is that the parser at first pass when encountering a definition of a function just looks for the closing paired curly bracket }, so it skips the function body altogether.

Values in strings

In manual: http://www.php.net/manual/en/language.types.string.php

Bash and PHP have one thing in common (well, probably on only one thing) - resolving variable values in strings:

"asdf $var asdf"
The other common thing is the problem with no spaces between the variable name and the rest of the string. E.g. let's assume the variable name is, like above, var.

"asdf$varasdf"
PHP will try to resolve the variable $varasdf, with no fallback to $varasd, $varas, etc (and rightly so! this would cause a whole new set of problems).
And this is the places where things get different. In Bash you have "asdf${var}asdf" while in PHP the dolar sign got inside the curly brackets:

"asdf{$var}asdf"
It's quite annoying when one uses Bash and PHP frequently.

The other thing is that you can actually execute functions inside the curly brackets in a string in PHP, but only if the function returns a name of a variable that's supposed to be resolved (or in case where you need to execute a PHP function and you can forge a PHP string - this is useful on the more offensive side of things).

"asdf{${func_that_returns_var()}}asdf"
That being said you cannot call a function and insert it's return value directly to a string from a string (it would be called "Inception" if that would be possible).
【 design & art by Xa / Gynvael Coldwind 】 【 logo font (birdman regular) by utopiafonts / Dale Harris 】