Home RipsTech PHP Security Calendar 2017 Notes
Post
Cancel

RipsTech PHP Security Calendar 2017 Notes

Notes related to RipsTech PHP Security Calendar 2017 which aren’t accessible anymore.

Challenge 1 - Wishlist

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)) {
            move_uploaded_file(
                $this->file['tmp_name'],
                self::UPLOAD_DIRECTORY . $this->file['name']
            );
        }
    }
}

$challenge = new Challenge($_FILES['solution']);
  • The vulnerability allows arbitrary file uploads, identified at line 13.
  • At line 12, in_array() is used to verify if the file name matches a number.
  • However, this check is not type-safe because the third parameter in in_array() is not set to true.
  • As a result, PHP will type-cast the file name to an integer when comparing it to the $whitelist array at line 8.
  • This allows bypassing the whitelist by prepending a number between 1 and 24 to the file name, such as “5backdoor.php”.
  • Successfully uploading this PHP file could lead to code execution on the server.

Challenge 2 - Twig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// composer require "twig/twig"
require 'vendor/autoload.php';

class Template {
    private $twig;

    public function __construct() {
        $indexTemplate = '<img ' .
            'src="https://loremflickr.com/320/240">' .
            '<a href="">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(
            'index.html',
            ['link' => $this->getNexSlideUrl()]
        );
    }
}

(new Template())->render();
  • A cross-site scripting (XSS) vulnerability exists at line 26.
  • Two filters aim to ensure the link passed to the tag is a legitimate URL:
  • filter_var() at line 22 checks for a valid URL.
  • Twig’s template escaping at line 10 helps prevent breaking out of the href attribute.
  • The vulnerability is still exploitable using a crafted URL like: ?nextSlide=javascript://comment%250aalert(1).
  • This payload contains no markup characters that Twig’s escaping would filter out and is also considered a valid URL by filter_var().
  • The payload uses a JavaScript protocol handler with a comment (//), followed by the JavaScript payload on a new line.
  • When the link is clicked, the JavaScript code executes in the victim’s browser.

Challenge 3 - Snowflake

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function __autoload($className) {
    include $className;
}

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

if (class_exists($controllerName)) {
    $controller = new $controllerName($data['t'], $data['v']);
    $controller->render();
} else {
    echo 'There is no page with this name';
}

class HomeController {
    private $template;
    private $variables;

    public function __construct($template, $variables) {
        $this->template = $template;
        $this->variables = $variables;
    }

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

This code contains two security vulnerabilities:

  1. File Inclusion Vulnerability
  • Triggered by the class_exists() call at line 8, which checks for the existence of a user-supplied class name.
  • This call invokes the custom autoloader at line 1 when the class name is unknown, leading to an attempt to include unknown classes.
  • An attacker can exploit this with a path traversal attack, for instance, by providing ../../../../etc/passwd as the class name to leak the system’s passwd file.
  • This vulnerability affects PHP versions up to 5.3.
  1. Arbitrary Object Instantiation
  • At line 9, the user-controlled class name is used to instantiate a new object, with the first argument of its constructor also under the attacker’s control.
  • This enables the invocation of arbitrary constructors within the PHP codebase.
  • Even if the code does not have a vulnerable constructor, PHP’s built-in SimpleXMLElement class can be used to initiate an XXE attack, potentially exposing sensitive files.

Example real world case: shopware-php-object-instantiation-to-blind-xxe

### Challenge 4 - False Beard

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 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 version="1.0"?>' .
                      '<user v="%s"/><pass v="%s"/>';
            $xml = sprintf($format, $user, $pass);
            $xmlElement = new SimpleXMLElement($xml);
            // Perform the actual login.
            $this->login($xmlElement);
        }
    }
}

new Login($_POST['username'], $_POST['password']);
  • An XML injection vulnerability exists at line 14, allowing an attacker to manipulate the XML structure and bypass authentication.
  • Lines 8 and 9 attempt to prevent exploitation by checking for angle brackets, but this check can be bypassed with a carefully crafted payload.
  • The vulnerability is due to PHP’s automatic type-casting:
  • The strpos() function returns the numeric position of the searched character, which can be 0 if the first character matches.
  • This 0 value is then type-cast to boolean false in the if comparison, causing the condition to evaluate as true.
  • A sample payload might look like: user=<"><injected-tag%20property="&pass;=<injected-tag>.

Challenge 5 - Postcard

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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'] = 'none@ripstech.com';
        } else {
            $data['to'] = $this->sanitize($data['to']);
        }

        if (!isset($data['from'])) {
            $data['from'] = 'none@ripstech.com';
        } 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();
$mailer->send($_POST);
  • A command execution vulnerability exists at line 31, where the $_POST[‘from’] variable is appended as the fifth parameter in the mail() function, allowing it to modify the sendmail command.
  • Although arbitrary commands cannot be executed, an attacker can append new parameters to sendmail, potentially creating a PHP backdoor via sendmail log files.
  • Insufficient Protections Against Exploitation - Two protections aim to prevent this exploit, but they are inadequate:
  • The sanitize() function first checks if the email address is valid at line 3. However, the filter does not block all characters required for exploiting the mail() function, allowing escaped whitespaces within double quotes.
  • At line 7, escapeshellarg() sanitizes the email address, which would normally suffice. However, PHP’s internal escape of the fifth parameter with escapeshellcmd() allows the attacker to bypass escapeshellarg() by escaping out of it.

Challenge 6 - Frost Pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class TokenStorage {
    public function performAction($action, $data) {
        switch ($action) {
            case 'create':
                $this->createToken($data);
                break;
            case 'delete':
                $this->clearToken($data);
                break;
            default:
                throw new Exception('Unknown action');
        }
    }

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

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

$storage = new TokenStorage();
$storage->performAction($_GET['action'], $_GET['data']);
  • A file delete vulnerability exists in this code.
  • The issue is caused by an unescaped hyphen (-) in the regular expression used in the preg_replace() call at line 21.
  • Without escaping, the hyphen acts as a range indicator, replacing any character not in a-z or within the ASCII range from dot (46) to underscore (95).
  • This allows directory traversal using . and /, enabling an attacker to delete (almost) arbitrary files.
  • For example, the parameters action=delete&data=../../config.php could delete the config.php file.

Challenge 7 - Bells

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function getUser($id) {
    global $config, $db;
    if (!is_resource($db)) {
        $db = new MySQLi(
            $config['dbhost'],
            $config['dbuser'],
            $config['dbpass'],
            $config['dbname']
        );
    }
    $sql = "SELECT username FROM users WHERE id = ?";
    $stmt = $db->prepare($sql);
    $stmt->bind_param('i', $id);
    $stmt->bind_result($name);
    $stmt->execute();
    $stmt->fetch();
    return $name;
}

$var = parse_url($_SERVER['HTTP_REFERER']);
parse_str($var['query']);
$currentUser = getUser($id);
echo '<h1>'.htmlspecialchars($currentUser).'</h1>';
  • A connection string injection vulnerability exists at line 4.
  • This issue arises from the parse_str() call at line 21, which behaves similarly to register_globals.
  • The parse_str() function extracts query parameters from the referrer as variables in the current scope, allowing control over the global $config variable within getUser() (lines 5–8).
  • This vulnerability allows an attacker to connect to a malicious MySQL server and return arbitrary values for the username. For example, using a referrer like http://host/?config[dbhost]=10.0.0.5&config[dbuser]=root&config[dbpass]=root&config[dbname]=malicious&id=1 enables this attack.

Challenge 8 - Candle

1
2
3
4
5
6
7
8
9
10
11
12
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";
  • A code injection vulnerability is present at line 4 due to the behavior of preg_replace() in versions of PHP prior to 7.
  • In these versions, preg_replace() includes an eval modifier (e), which treats the replacement (second parameter) as PHP code if enabled.
  • Although direct injection into the second parameter isn’t possible, the \\1 placeholder (representing the matched expression) can be controlled.
  • While it’s not possible to escape the strtolower() function call, the matched value is within double quotes, allowing the use of PHP’s curly syntax for injecting additional function calls.
  • An example attack payload could look like: /?=.*={${phpinfo()}}, enabling execution of phpinfo().

Challenge 9 - Rabbit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class LanguageManager
{
    public function loadLanguage()
    {
        $lang = $this->getBrowserLanguage();
        $sanitizedLang = $this->sanitizeLanguage($lang);
        require_once("/lang/$sanitizedLang");
    }

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

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

(new LanguageManager())->loadLanguage();
  • A file inclusion vulnerability exists, which could allow an attacker to execute arbitrary code or leak sensitive files on the server.
  • The issue is found in the sanitization function at line 18, where the ../ string is replaced but not recursively.
  • This failure allows an attacker to use alternate sequences like ....// or ..././, which will still resolve to ../ after replacement.
  • As a result, the attacker can perform path traversal and manipulate the path to the included language file.
  • For example, the system’s passwd file can be leaked by setting the following payload in the Accept-Language HTTP request header: .//....//....//etc/passwd.

Challenge 10 - Anticipation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extract($_POST);

function goAway() {
    error_log("Hacking attempt.");
    header('Location: /error/');
}

if (!isset($pi) || !is_numeric($pi)) {
    goAway();
}

if (!assert("(int)$pi == 3")) {
    echo "This is not pi.";
} else {
    echo "This might be pi.";
}
  • A code injection vulnerability exists at line 12, allowing an attacker to execute arbitrary PHP code on the web server.
  • The assert() function evaluates PHP code and contains user input, creating the potential for malicious code execution.
  • At line 1, PHP’s built-in extract() function instantiates all POST parameters as global variables, which can lead to security issues by exposing input directly as variables.
  • In this challenge, the vulnerability allows the attacker to set the $pi variable directly via a POST parameter.
  • At line 8, there is a check to ensure the input is numeric. If not, the user is redirected to an error page via the goAway() function.
  • However, after the redirect in line 5, the script continues execution because there is no exit() call, allowing the attacker to inject and execute PHP code in the $pi parameter.
  • For example, setting pi=phpinfo() will cause the phpinfo() function to execute.

Challenge 11 - Pumpkin Pie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Template {
    public $cacheFile = '/tmp/cachefile';
    public $template = '<div>Welcome back %s</div>';

    public function __construct($data = null) {
        $data = $this->loadData($data);
        $this->render($data);
    }

    public function loadData($data) {
        if (substr($data, 0, 2) !== 'O:'
        && !preg_match('/O:\d:\/', $data)) {
            return unserialize($data);
        }
        return [];
    }

    public function createCache($file = null, $tpl = null) {
        $file = $file ?? $this->cacheFile;
        $tpl = $tpl ?? $this->template;
        file_put_contents($file, $tpl);
    }

    public function render($data) {
        echo sprintf(
            $this->template,
            htmlspecialchars($data['name'])
        );
    }

    public function __destruct() {
        $this->createCache();
    }
}

new Template($_COOKIE['data']);
  • This challenge contains a PHP object injection vulnerability, found at line 13, where an attacker can pass user input into the unserialize() function by modifying their cookie data.

  • There are two checks at lines 11 and 12 to prevent object deserialization:
    1. The first check can be easily bypassed by injecting an object into an array, resulting in a payload string that begins with a:1: instead of O:.
    2. The second check can be bypassed by exploiting PHP’s flexible serialization syntax, using O:+1: to bypass the regex check.
  • This allows the attacker to inject an object of class Template into the application.

  • After the serialized object is deserialized and the Template object is instantiated, its destructor is triggered when the script terminates (line 31).

  • The attacker-controlled properties, cacheFile and template, are used in line 21 to write to a file, enabling the creation of arbitrary files on the system.

  • For example, an attacker can create a PHP shell in the document root with the payload:
    a:1:{i:0;O:%2b8:"Template":2:{s:9:"cacheFile";s:14:"/var/www/a.php";s:8:"template";s:16:"<?php%20phpinfo();";}}

  • More information about this attack can be found in this blog post.

Challenge 12 - String Lights

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$sanitized = [];

foreach ($_GET as $key => $value) {
    $sanitized[$key] = intval($value);
}

$queryParts = array_map(function ($key, $value) {
    return $key . '=' . $value;
}, array_keys($sanitized), array_values($sanitized));

$query = implode('&', $queryParts);

echo "<a href='/images/size.php?" .
    htmlentities($query) . "'>link</a>";
  • This challenge contains a cross-site scripting (XSS) vulnerability, found at line 13.

  • The issue arises from the insufficient sanitization of the keys in the $_GET array (the GET parameter names).

  • Both the keys and sanitized GET values are concatenated into the href attribute of the <a> tag.

  • The htmlentities() function is used to sanitize the input, but it does not affect single quotes by default.

  • As a result, an attacker can exploit this by injecting an XSS payload into the parameter name, which breaks the href attribute and appends a JavaScript event handler.

  • For example, the following query parameter can be used:
    /a'onclick%3dalert(1)%2f%2f=c

  • The payload is placed in the parameter name, not the parameter value, allowing the attacker to execute JavaScript in the user’s browser.

Challenge 13 - Turkey Blaster

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class LoginManager {
    private $em;
    private $user;
    private $password;

    public function __construct($user, $password) {
        $this->em = DoctrineManager::getEntityManager();
        $this->user = $user;
        $this->password = $password;
    }

    public function isValid() {
        $user = $this->sanitizeInput($this->user);
        $pass = $this->sanitizeInput($this->password);
        
        $queryBuilder = $this->em->createQueryBuilder()
            ->select("COUNT(p)")
            ->from("User", "u")
            ->where("user = '$user' AND password = '$pass'");
        $query = $queryBuilder->getQuery();
        return boolval($query->getSingleScalarResult());
    }
	
    public function sanitizeInput($input, $length = 20) {
        $input = addslashes($input);
        if (strlen($input) > $length) {
            $input = substr($input, 0, $length);
        }
        return $input;
    }
}

$auth = new LoginManager($_POST['user'], $_POST['passwd']);
if (!$auth->isValid()) {
    exit;
}
  • Today’s challenge contains a Doctrine Query Language (DQL) injection vulnerability, located in line 19.

  • DQL injection is similar to SQL injection but more limited. In this case, the where() method of Doctrine is vulnerable.

  • In lines 13 and 14, sanitization is applied to the input, but the sanitizeInput() method contains a bug.

  • The method uses addslashes() to escape relevant characters by adding a backslash (\) in front of them. However, this causes an issue when a backslash (\) is passed as input, as it gets escaped to \\.

  • The substr() function is then used to truncate the string, which can result in an escaped backslash being cut off, leaving a single backslash (\) at the end of the string.

  • This improperly escaped input can break the WHERE statement and allow an attacker to inject custom DQL syntax. For example, using the condition OR 1=1, which always evaluates to true, can bypass authentication: user=1234567890123456789\&passwd;=%20OR%201=1-

  • The resulting DQL query becomes:
    user = '1234567890123456789\' AND password = ' OR 1=1-'

  • The backslash confuses the quotes, enabling DQL injection into the password value.

  • The resulting query is technically invalid due to the trailing slash, but Doctrine automatically closes the last single quote, resulting in the final query:
    OR 1=1-''

  • To prevent DQL injections, always use bound parameters for dynamic conditions and avoid using addslashes() or similar functions to secure queries.

  • Additionally, store passwords in a hashed format (e.g., BCrypt) in the database.

Challenge 14 - Snowman

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Carrot {
    const EXTERNAL_DIRECTORY = '/tmp/';
    private $id;
    private $lost = 0;
    private $bought = 0;

    public function __construct($input) {
        $this->id = rand(1, 1000);

        foreach ($input as $field => $count) {
            $this->$field = $count++;
        }
    }

    public function __destruct() {
        file_put_contents(
            self::EXTERNAL_DIRECTORY . $this->id,
            var_export(get_object_vars($this), true)
        );
    }
}

$carrot = new Carrot($_GET);
  • This class is vulnerable to directory traversal due to mass assignment.

  • The constructor (line 11) allows arbitrary class attributes to be set via user input. By overwriting the $id attribute, an attacker can control the first parameter of file_put_contents() in line 16.

  • By using ../, the attacker can target arbitrary files on the system that are writable, potentially allowing them to create a PHP shell in the document root.

  • The class assigns and increments values in line 11. However, the incrementation happens after the assignment, meaning the class attribute still holds the original value of $count after the operation.

  • To prevent this security issue:
    • Be very cautious when using reflection to set variables based on user input.
    • Implement a whitelist verification that only allows specific variable names to be modified.
  • A real-world example of a vulnerability caused by mass assignment can be found here.

Challenge 15 - Sleigh Ride

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Redirect {
    private $websiteHost = 'www.example.com';

    private function setHeaders($url) {
        $url = urldecode($url);
        header("Location: $url");
    }

    public function startRedirect($params) {
        $parts = explode('/', $_SERVER['PHP_SELF']);
        $baseFile = end($parts);
        $url = sprintf(
            "%s?%s",
            $baseFile,
            http_build_query($params)
        );
        $this->setHeaders($url);
    }
}

if ($_GET['redirect']) {
    (new Redirect())->startRedirect($_GET['params']);
}

This challenge contains an open redirect vulnerability in line 6.

The code:

  • Takes input from the $_SERVER['PHP_SELF'] superglobal.
  • Splits the input at the slash character (/) on line 10.
  • Uses the last part to construct a new URL.
  • Passes the new URL to the header() function.

An attacker can exploit this vulnerability by injecting a malicious URL using URL-encoded characters, which are decoded on line 5.

A possible payload could look like this: /index.php/http:%252f%252fwww.domain.com?redirect=1

Impact:

  • Phishing Attacks: Redirecting users to malicious sites disguised as the original site.
  • Credential Theft: Tricking users into entering their credentials on fake login pages.

Challenge 16 - Poem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class FTP {
    public $sock;

    public function __construct($host, $port, $user, $pass) {
        $this->sock = fsockopen($host, $port);

        $this->login($user, $pass);
        $this->cleanInput();
        $this->mode($_REQUEST['mode']);
        $this->send($_FILES['file']);
    }

    private function cleanInput() {
        $_GET = array_map('intval', $_GET);
        $_POST = array_map('intval', $_POST);
        $_COOKIE = array_map('intval', $_COOKIE);
    }

    public function login($username, $password) {
        fwrite($this->sock, "USER " . $username . "\n");
        fwrite($this->sock, "PASS " . $password . "\n");
    }

    public function mode($mode) {
        if ($mode == 1 || $mode == 2 || $mode == 3) {
            fputs($this->sock, "MODE $mode\n");
        }
    }

    public function send($data) {
        fputs($this->sock, $data);
    }
}

new FTP('localhost', 21, 'user', 'password');

This challenge presents two vulnerabilities that, when combined, allow for data injection into an open FTP connection.

Bug #1: Incomplete Sanitization (Line 9)

  • The code utilizes $_REQUEST in line 9 to capture user input.
  • Lines 14-16 sanitize only $_GET and $_POST, neglecting $_COOKIE data.
  • Crucially, $_REQUEST is a combined copy of these inputs, not a reference.
  • This incomplete sanitization leaves $_REQUEST vulnerable to malicious content.

Real-World Impact:

A similar vulnerability in WordPress security is documented on our blog (archived link: https://web.archive.org/web/20171224161253/https://blog.ripstech.com/2016/the-state-of-wordpress-security/#all-in-one-wp-security-firewall).

Bug #2: Type Juggling with == (Line 25)

  • Line 25 employs the type-unsafe comparison operator == instead of the strict comparison ===.
  • This allows attackers to inject malicious code through type juggling.
  • Example Payload: ?mode=1%0a%0dDELETE%20test.file
    • Decoded, the payload injects a “DELETE” command potentially wiping out a file named “test.file”.

Combined Impact:

These vulnerabilities enable attackers to:

  • Inject arbitrary commands into the FTP connection.
  • Potentially manipulate or steal sensitive data.

Challenge 17 - Mistletoe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class RealSecureLoginManager {
    private $em;
    private $user;
    private $password;

    public function __construct($user, $password) {
        $this->em = DoctrineManager::getEntityManager();
        $this->user = $user;
        $this->password = $password;
    }

    public function isValid() {
        $pass = md5($this->password, true);
        $user = $this->sanitizeInput($this->user);

        $queryBuilder = $this->em->createQueryBuilder()
            ->select("COUNT(p)")
            ->from("User", "u")
            ->where("password = '$pass' AND user = '$user'");
        $query = $queryBuilder->getQuery();
        return boolval($query->getSingleScalarResult());
    }

    public function sanitizeInput($input) {
        return addslashes($input);
    }
}

$auth = new RealSecureLoginManager(
    $_POST['user'],
    $_POST['passwd']
);
if (!$auth->isValid()) {
    exit;
}
  • The challenge is intended to be a fixed version of Day 13 but inadvertently introduces new security flaws.
  • The author attempted to address the DQL injection by using addslashes() on the username but forgot to use substr().
  • The password was hashed using md5() on line 13, which is not recommended for secure password storage.
    • md5() is not secure for password hashing and should be avoided.
    • Password hashes should not be directly compared.
    • The second parameter for md5() is set to true, returning the hash in binary format, which can contain ASCII characters.
  • When the hash is binary, Doctrine may misinterpret certain characters, leading to potential issues.
    • In this case, an attacker could use 128 as the password, resulting in a hash like v�an���l���q��\, where the backslash escapes a single quote, enabling a DQL injection.
    • A potential attack could use this payload: ?user=%20OR%201=1-&passwd;=128.
  • To prevent DQL injections, always use bound parameters for dynamic conditions in queries.
  • Never attempt to “secure” a DQL query with addslashes() or similar functions.
  • Passwords should be hashed using a secure algorithm, such as BCrypt, rather than insecure methods like md5().

Challenge 18 - Sign

1
2
3
4
5
6
7
8
9
10
11
12
class JWT {
    public function verifyToken($data, $signature) {
        $pub = openssl_pkey_get_public("file://pub_key.pem");
        $signature = base64_decode($signature);
        if (openssl_verify($data, $signature, $pub)) {
            $object = json_decode(base64_decode($data));
            $this->loginAsUser($object);
        }
    }
}

(new JWT())->verifyToken($_GET['d'], $_GET['s']);
  • The challenge includes a bug in the use of the openssl_verify() function on line 5, which causes an authentication bypass on line 7.
  • The openssl_verify() function returns three possible values:
    • 1 if the signature is correct,
    • 0 if the signature verification fails,
    • -1 if an error occurs during the verification process.
  • The issue arises when an attacker generates a valid signature using a different algorithm than the one used by pub_key.pem.
    • In this case, openssl_verify() returns -1, which is automatically cast to true, bypassing the authentication check.
  • To resolve this, use a type-safe comparison (===) to validate the return value of openssl_verify().
  • Alternatively, consider using a more secure cryptography library to handle signature verification.

Challenge 19 - Birch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ImageViewer {
    private $file;

    function __construct($file) {
        $this->file = "images/$file";
        $this->createThumbnail();
    }

    function createThumbnail() {
        $e = stripcslashes(
            preg_replace(
                '/[^0-9\\\]/',
                '',
                isset($_GET['size']) ? $_GET['size'] : '25'
            )
        );
        system("/usr/bin/convert {$this->file} --resize $e
                ./thumbs/{$this->file}");
    }

    function __toString() {
        return "<a href={$this->file}>
                <img src=./thumbs/{$this->file}></a>";
    }
}

echo (new ImageViewer("image.png"));
  • The ImageViewer class is vulnerable to remote command execution via the size parameter on line 17.
  • The preg_replace() function is used to remove most non-digit characters, but this is not enough to prevent exploitation.
  • The stripcslashes() function not only removes slashes but also converts C literal escape sequences into their actual byte values.
    • This leaves the backslash character unaffected by preg_replace(), allowing an attacker to inject an octal byte escape sequence like 0\073\163\154\145\145\160\0405\073.
  • The stripcslashes() function will evaluate the input to 0;sleep 5;, which gets appended to the system command and executed by the attacker.

Challenge 20 - Stocking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
set_error_handler(function ($no, $str, $file, $line) {
    throw new ErrorException($str, 0, $no, $file, $line);
}, E_ALL);

class ImageLoader
{
    public function getResult($uri)
    {
        if (!filter_var($uri, FILTER_VALIDATE_URL)) {
            return '<p>Please enter valid uri</p>';
        }

        try {
            $image = file_get_contents($uri);
            $path = "./images/" . uniqid() . '.jpg';
            file_put_contents($path, $image);
            if (mime_content_type($path) !== 'image/jpeg') {
                unlink($path);
                return '<p>Only .jpg files allowed</p>';
            }
        } catch (Exception $e) {
            return '<p>There was an error: ' .
                $e->getMessage() . '</p>';
        }

        return '<img src="' . $path . '" width="100"/>';
    }
}

echo (new ImageLoader())->getResult($_GET['img']);
  • This challenge contains a server-side request forgery (SSRF) vulnerability that allows an attacker to make requests on behalf of the targeted web server.
  • This enables the attacker to access servers that would otherwise be unreachable from the outside, such as internal systems behind the web server.
    • For example, an attacker could use this vulnerability to conduct a port scan or retrieve banners (like server versions) from internal services.
  • The vulnerability is caused by two factors:
    • The use of file_get_contents() with unfiltered user input on line 14.
    • The printing of error messages to the user on line 23.
  • An attacker could exploit this by providing an internal URI like ?img=http://internal:22, which would return a response such as failed to open stream: HTTP request failed! SSH-2.0-OpenSSH_7.2p2 Ubuntu-4ubuntu2.2 if OpenSSH is running.
    • This kind of information can aid in further attacks.
  • Another common attack scenario is retrieving sensitive AWS credentials when exploiting an AWS cloud instance.
  • Additionally, filter_var() accepts file:// URLs, which can allow an attacker to load local files.

Challenge 21 - Gift Wrap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
declare(strict_types=1);

class ParamExtractor {
    private $validIndices = [];

    private function indices($input) {
        $validate = function (int $value, $key) {
            if ($value > 0) {
                $this->validIndices[] = $key;
            }
        };

        try {
            array_walk($input, $validate, 0);
        } catch (TypeError $error) {
            echo "Only numbers are allowed as input";
        }

        return $this->validIndices;
    }

    public function getCommand($parameters) {
        $indices = $this->indices($parameters);
        $params = [];
        foreach ($indices as $index) {
            $params[] = $parameters[$index];
        }
        return implode($params, ' ');
    }
}

$cmd = (new ParamExtractor())->getCommand($_GET['p']);
system('resizeImg image.png ' . $cmd);
  • This challenge contains a command injection vulnerability on line 33.
  • The developer enabled strict_types=1 on line 1 to enforce type hints in the validate function (line 7), ensuring that a TypeError is thrown if a non-integer value is passed.
  • Despite strict typing being enabled, there is a bug in the use of array_walk(), which bypasses strict typing and instead uses PHP’s default weak typing.
  • As a result, an attacker can append a command to the last parameter, which will be executed in the system call.
    • A possible exploit could look like ?p[1]=1&p[2]=2;%20ls%20-la.

Challenge 22 - Chimney

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (isset($_POST['password'])) {
    setcookie('hash', md5($_POST['password']));
    header("Refresh: 0");
    exit;
}

$password = '0e836584205638841937695747769655';
if (!isset($_COOKIE['hash'])) {
    echo '<form><input type="password" name="password" />'
       . '<input type="submit" value="Login" ></form >';
    exit;
} elseif (md5($_COOKIE['hash']) == $password) {
    echo 'Login succeeded';
} else {
    echo 'Login failed';
}
  • The code snippet has four vulnerabilities:
    1. Type-unsafe comparison: On line 12, the hashed password is compared to a string using a type-unsafe operator. The string is in scientific notation and is interpreted as “zero to the power of X”, which results in zero. If an attacker can generate a zero-string for the hashed user input, the comparison will succeed.
      • These hashes are known as “Magic Hashes.” A Google search reveals that the MD5 hash of 240610708 produces the desired properties.
    2. Hashing issue: The password hash is calculated twice, which prevents directly submitting the value. The second vulnerability arises because the first hash is calculated on the server but stored in a cookie on the client side. The attacker can inject the value 240610708 directly into the password cookie to bypass the check.
    3. Timing attack vulnerability: The comparison of the hashes is vulnerable to timing attacks. To mitigate this, the hash_equals() function should be used for secure comparison.
    4. Weak password hashing: The code uses the MD5 algorithm to hash the password, which is considered broken and not suitable for password storage. A more secure algorithm like BCrypt should be used for hashing passwords.
  • Additionally, passwords should not be hardcoded in the code but should be stored in a configuration file for better security.

Challenge 23 - Cookies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class LDAPAuthenticator {
    public $conn;
    public $host;

    function __construct($host = "localhost") {
        $this->host = $host;
    }

    function authenticate($user, $pass) {
        $result = [];
        $this->conn = ldap_connect($this->host);    
        ldap_set_option(
            $this->conn,
            LDAP_OPT_PROTOCOL_VERSION,
            3
        );
        if (!@ldap_bind($this->conn))
            return -1;
        $user = ldap_escape($user, null, LDAP_ESCAPE_DN);
        $pass = ldap_escape($pass, null, LDAP_ESCAPE_DN);
        $result = ldap_search(
            $this->conn,
            "",
            "(&(uid=$user)(userPassword=$pass))"
        );
        $result = ldap_get_entries($this->conn, $result);
        return ($result["count"] > 0 ? 1 : 0);
    }
}

if(isset($_GET["u"]) && isset($_GET["p"])) {
    $ldap = new LDAPAuthenticator();
    if ($ldap->authenticate($_GET["u"], $_GET["p"])) {
        echo "You are now logged in!";
    } else {
        echo "Username or password unknown!";
    }
}
  • The LDAPAuthenticator class is vulnerable to an LDAP injection on line 24.
  • By injecting special characters into the username, an attacker can manipulate the LDAP query’s result set.
  • Although the ldap_escape() function is used to sanitize input on lines 19 and 20, an incorrect flag is passed to the function, leading to improper sanitization.
  • As a result, the LDAP injection allows an unauthenticated attacker to bypass the authentication mechanism.
    • In this case, the attacker can inject the asterisk wildcard * as both the username and password to successfully authenticate as any user.

Challenge 24 - Nutcracker

1
2
3
4
5
6
@$GLOBALS=$GLOBALS{next}=next($GLOBALS{'GLOBALS'})
[$GLOBALS['next']['next']=next($GLOBALS)['GLOBALS']]
[$next['GLOBALS']=next($GLOBALS[GLOBALS]['GLOBALS'])
[$next['next']]][$next['GLOBALS']=next($next['GLOBALS'])]
[$GLOBALS[next]['next']($GLOBALS['next']{'GLOBALS'})]=
next(neXt(${'next'}['next']));
  • This challenge involves a code snippet created by one of our team members for the Hack.lu CTF Tournament.
  • The code relies heavily on the next() function and the $GLOBALS array:
    • The next() function advances the internal array pointer by one position.
    • Combined with the $GLOBALS array, this setup allows for the execution of arbitrary code.
  • The payload is divided into two parts:
    1. The first part is a PHP function to execute, passed via $_COOKIE['GLOBALS'].
    2. The second part consists of parameters for the function, injected through the file type of a file sent with the same name as the PHP function.
  • For a more detailed explanation of the solution, you can refer to this write-up:
    Hack.lu CTF 2014 - Next Global Backdoor Write-Up

Link to archived writeup:

This post is licensed under CC BY 4.0 by the author.