This is my first time participating in an Intigriti challenge. Personally, I find this to be a very interesting challenge involving PHP and SQLi.

Enter the challenge details, we will be provided with a website at: https://challenge-0923.intigriti.io/challenge.php

Untitled

Brief analysis reveals that this is a website that allows listing the users in the database and has a button to view the server-side source code.

Untitled

When clicking on the "show source" button, we get the following PHP code:

<?php

if (isset($_GET['showsource'])) {
    highlight_file(__FILE__);
    exit();
}

require_once("config.php");

$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];

try {
    $pdo = new PDO($dsn, $user, $pass, $options);
} catch (\\PDOException $e) {
    exit("Unable to connect to DB");
}

$max = 10;

if (isset($_GET['max']) && !is_array($_GET['max']) && $_GET['max']>0) {
    $max = $_GET['max'];
    $words  = ["'","\\"",";","`"," ","a","b","h","k","p","v","x","or","if","case","in","between","join","json","set","=","|","&","%","+","-","<",">","#","/","\\r","\\n","\\t","\\v","\\f"]; // list of characters to check
    foreach ($words as $w) {
        if (preg_match("#".preg_quote($w)."#i", $max)) {
            exit("H4ckerzzzz");
        } //no weird chars
    }       
}

try{
//seen in production
$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE id<=$max");
$stmt->execute();
$results = $stmt->fetchAll();
}
catch(\\PDOException $e){
    exit("ERROR: BROKEN QUERY");
}
    /* FYI
    CREATE TABLE users (
        id INT AUTO_INCREMENT PRIMARY KEY,
        name VARCHAR(255) NOT NULL,
        email VARCHAR(255) UNIQUE NOT NULL,
        password VARCHAR(255) NOT NULL
    );
    */
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Utenti</title>
    <link href="<https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css>" rel="stylesheet">
</head>
<div class="container mt-5">

    <h2>Users</h2>

    <table class="table table-bordered">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Email</th>
            </tr>
        </thead>
        <tbody>
            <?php foreach ($results as $row): ?>
                <tr>
                    <td><?= htmlspecialchars(strpos($row['id'],"INTIGRITI")===false?$row['id']:"REDACTED"); ?></td> 
                    <td><?= htmlspecialchars(strpos($row['name'],"INTIGRITI")===false?$row['name']:"REDACTED"); ?></td>
                    <td><?= htmlspecialchars(strpos($row['email'],"INTIGRITI")===false?$row['email']:"REDACTED"); ?></td>
                </tr>
            <?php endforeach; ?>
        </tbody>
    </table>

    <div class="text-center mt-4">
        <!-- Show Source Button -->
        <a href="?showsource=1" class="btn btn-primary">Show Source</a>
    </div>

</div>

<!-- including Bootstrap e jQuery -->
<script src="<https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js>"></script>
<script src="<https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js>"></script>

</body>
</html>

Proceed with the analysis, and it is observed that the max parameter can be used to increase the number of returned records through the following code snippet:

$max = 10;

if (isset($_GET['max']) && !is_array($_GET['max']) && $_GET['max']>0) {
    $max = $_GET['max'];
    $words  = ["'","\\"",";","`"," ","a","b","h","k","p","v","x","or","if","case","in","between","join","json","set","=","|","&","%","+","-","<",">","#","/","\\r","\\n","\\t","\\v","\\f"]; // list of characters to check
    foreach ($words as $w) {
        if (preg_match("#".preg_quote($w)."#i", $max)) {
            exit("H4ckerzzzz");
        } //no weird chars
    }       
}

try{
//seen in production
$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE id<=$max");
$stmt->execute();
$results = $stmt->fetchAll();
}
catch(\\PDOException $e){
    exit("ERROR: BROKEN QUERY");
}

It is observed that when performing query creation, the server will create a query by concatenating strings from the variable $max. We can control the value of the variable $max through the max parameter with a GET request ⇒ SQL injection can be exploited.

However, before being passed into the query, the variable $max will be filtered in the if-else statement. Specifically, as follows:

  1. Check the following conditions:
    1. Does the param max appear in the GET request?
    2. Is the param max an array?
    3. Does the value of param max exceed 0?
  2. If all the above conditions are met, assign the value of param max to the variable $max.
  3. Create an array $words containing blacklisted characters.
  4. Iterate through each character in the $words array and check if that character appears in $max.
  5. If a word in $words appears in $max, it will stop executing the code and print the message "H4ckerzzzz".

First, based on the blacklist, it can be seen that we will not be able to use certain functions and keywords such as substring, substr, if, between, in, .... and certain speical characters like ', space, /, \\ in MySQL.

Next, analyze the code segment for printing the query result:

<tbody>
            <?php foreach ($results as $row): ?>
                <tr>
                    <td><?= htmlspecialchars(strpos($row['id'],"INTIGRITI")===false?$row['id']:"REDACTED"); ?></td> 
                    <td><?= htmlspecialchars(strpos($row['name'],"INTIGRITI")===false?$row['name']:"REDACTED"); ?></td>
                    <td><?= htmlspecialchars(strpos($row['email'],"INTIGRITI")===false?$row['email']:"REDACTED"); ?></td>
                </tr>
            <?php endforeach; ?>
        </tbody>

The flag has the form INTIGRITI{.*}, but when reading the records returned from the database, the server will check if the string INTIGRITI exists in those records. If it does, it will only print out the string "REDACTED". Therefore, we need to remove the string INTIGRITI from the result when performing union-based SQLi or use time-based SQLi.