1837 words
9 minutes
Local CTF 1337 — Smarty SSTI, IDOR & OSINT

Event: Local CTF Tournament — 1337 School (42 Network)
Target Categories: Advanced Web Exploitation & Misc (Forensics / OSINT / Cryptography)
Source Code & Complete Repository: onevilx/Writeups - Local CTF 1337


1. Executive Summary & Tournament Triage

Competitive cybersecurity events hosted inside 1337 School (42 Network) are legendary for their realistic offensive security engineering challenges. Rather than relying on artificial guesswork, challenges challenge operators to combine rigorous code auditing, API traffic dissection, custom Python exploitation tooling, and investigative OSINT techniques.

In this deep-dive technical publication, I present my complete, verified writeups and underlying exploit code for four standout challenges from the recent Local CTF tournament: two intricate Web Exploitation problems (Notebook and new intra) and two sophisticated Misc/Forensics evaluations (July Pool 2024 and Waya Dazai khsna nl9aw stage).


2. Web Challenge 1: Notebook (Smarty SSTI & WAF Bypass)

  • Category: Web
  • Difficulty: Easy
  • Vulnerability: Server-Side Template Injection (Smarty PHP) + Custom WAF Evasion

2.1 Reconnaissance & Backend Discovery

In the Notebook challenge, we are presented with a simple blog interface where students can post comments and test markdown rendering via an active preview engine. Whenever user input is reflected directly into an HTML rendering container, testing for Server-Side Template Injection (SSTI) becomes priority number one.

Submitting the test expression {{7*7}} caused the server to render 49 directly into the DOM! To map the specific server framework, I conducted structural probe tests and confirmed the underlying server runtime was running PHP driven by the Smarty Templating Engine.

+-----------------------------------------------------------------------------+
| NOTEBOOK SMARTY SSTI WAF BYPASS ARCHITECTURE |
+-----------------------------------------------------------------------------+
| WAF Blocklist: system, exec, flag, {php}, $smarty, /etc, base64, eval... |
| Length Limit: <= 200 characters | Max Pipes '|': <= 3 |
| |
| Evasion Strategy (Dynamic String Concat + Native PHP Function Piping): |
| Step 1: {assign var="x" value="/f"|cat:"lag.txt"} -> $x = "/flag.txt" |
| Step 2: {$x|file_get_contents} -> Reads flag cleanly! |
| |
| [Captured Output] -> leet{sm4r7y_7pl_1nj3c710n_n0_w4f_c4n_s70p_m3} |
+-----------------------------------------------------------------------------+

2.2 Auditing the Custom WAF & Code Restrictions

Inspecting the application source revealed that user inputs submitted via the text GET parameter pass through a custom Web Application Firewall (WAF) before executing inside a Smarty string: evaluation resource ($smarty->fetch('string:' . $tpl_string)):

$BLOCKLIST = [
'system', 'exec', 'passthru', 'shell_exec', 'popen', 'proc_open', 'pcntl_exec',
'eval', 'assert', '\{php\}', '\{\/php\}', 'flag', '\/etc', 'proc', '\$smarty',
'base64', 'hex2bin', 'call_user_func', 'preg_replace', 'create_function',
'include', 'require'
];
if (strlen($text) > 200 || substr_count($text, '|') > 3 || is_blocked($text, $BLOCKLIST)) {
die("WAF: blocked pattern detected or input constraints breached.");
}

The WAF blocks typical Smarty command execution tricks: we cannot use {php}system('id'){/php}, we cannot access the built-in $smarty environmental array, we cannot invoke system or exec, and the literal string flag is completely banned! Moreover, input length is strictly restricted to 200 characters with a maximum of 3 piping filter operators (|).

2.3 Crafting the Bypassed SSTI Payload

To bypass keyword detection while adhering to tight character limits, we can leverage Smarty’s native variable assignment ({assign}) and string concatenation filter (|cat:). By splitting the word "flag.txt" into disconnected string segments ("/f" and "lag.txt"), we slip straight through regular expression blocklists!

Once our path string is stored inside a runtime variable ($x), we utilize a single remaining Smarty modifier pipe to feed the target filepath directly into an unblocked native PHP filesystem reader: file_get_contents!

{assign var="x" value="/f"|cat:"lag.txt"}{$x|file_get_contents}

Final Exploit Request URL:

GET /?action=preview&text={assign%20var=%22x%22%20value=%22/f%22|cat:%22lag.txt%22}{$x|file_get_contents} HTTP/1.1
Host: 104.199.105.242:4242

Submitting this URL forces Smarty to assemble the target string dynamically in system memory and immediately prints our winning flag directly inside the HTML preview response: leet{sm4r7y_7pl_1nj3c710n_n0_w4f_c4n_s70p_m3}\mathbf{leet\{sm4r7y\_7pl\_1nj3c710n\_n0\_w4f\_c4n\_s70p\_m3\}}


3. Web Challenge 2: new intra (IDOR & Mass Assignment)

  • Category: Web
  • Difficulty: Medium
  • Vulnerability: IDOR combined with Mass Assignment leading to Admin Account Takeover

3.1 Portal Architecture & Access Mapping

This blackbox engagement models a student administration platform featuring profile customization, a dedicated /staff login portal, and a protected /admin dashboard. Initial testing against the /admin destination returned a rigid 403 Forbidden error response—confirming that only high-privilege staff accounts (specifically the legendary root administrator account: bocal) are allowed entry.

While auditing the user profile settings dashboard, I observed that modifying basic profile attributes (such as changing an academic motto or profile avatar) triggered asynchronous JSON PUT requests directed at an authenticated API endpoint:

PUT /api/profile/153 HTTP/1.1
Host: intra.challenge.local
Content-Type: application/json
{"title": "writer's soul", "bio": "Hunting bugs across 1337 systems."}

3.2 IDOR Discovery & Built-in Account Privilege Reset

Notice the explicit numeric identifier appended to the routing path (/api/profile/153). To test for Insecure Direct Object References (IDOR), I intercepted the request in Burp Suite and swapped my profile ID (153) for targeting neighboring accounts. While standard user profiles rejected unauthorized modification attempts, continuous fuzzing revealed an incredible architectural oversight: built-in core staff accounts (IDs 1 through 6) lacked proper access control validation bindings!

Targeting account ID 1 (the foundational bocal admin account), I combined the IDOR vulnerability with a classic Mass Assignment injection—forcefully appending a "password" update attribute into the transmitted payload body:

{
"title": "Compromised by onevilx",
"bio": "Account Taken Over",
"password": "test"
}

Submitting this combined exploit payload against PUT /api/profile/1 returned a triumphant 200 OK success confirmation! I immediately navigated to the /staff sign-in gateway, authenticated using the target username bocal and my freshly injected password (test), and unlocked complete unrestricted administrative access into the /admin dashboard to capture the flag!


4. Misc Challenge 1: July Pool 2024 (Git Object Forensics)

  • Category: Forensics / Git Mechanics
  • Difficulty: Easy
  • Vulnerability: Sensitive Data Recovery via Unreachable Git Objects & Dangling Commits

4.1 Unpacking the Piscine Archive

In this forensics challenge, we receive an archive named pool.zip containing 1337 bootcamp source files along with a concealed version control repository (.git). When inexperienced developers clean sensitive secrets out of source codes, they mistakenly assume that executing git rm file or performing hard history resets erases historical records forever.

Upon extracting the archive and inspecting commit logs via git log -p | grep -i "leet{" and checking remote branches via git log --all -p, standard queries returned zero matches. The target secret had been completely expunged from all active project timelines!

4.2 Mining Orphaned Blobs & Dangling Commits

When commits are orphaned via hard branch resets or rebase actions, underlying data blobs remain preserved inside the .git/objects/ internal cryptographic database until an aggressive garbage collection pruning loop (git gc) executes. To inspect unreachable database structures, I invoked Git’s internal file system check tool:

Terminal window
# Recover unreachable repository blobs and verify internal object integrity
git fsck --unreachable
git fsck --lost-found
+-----------------------------------------------------------------------------+
| GIT FORENSICS RECOVERY WORKFLOW |
+-----------------------------------------------------------------------------+
| 1. Standard search: git log --all -p -> Result: 0 matches (History purged) |
| 2. File System Check: git fsck --unreachable |
| [Output] -> Uncovers multiple dangling commits & unlinked blobs! |
| 3. Dump Object Memories: git show <dangling_hash> |
| 4. Reconstruct fragmented flag string from orphaned repository blobs! |
+-----------------------------------------------------------------------------+

The fsck operation successfully extracted numerous unlinked dangling commits and restored orphaned files into .git/lost-found/. By writing a simple bash iteration loop invoking git show across every single uncovered dangling commit hash, I identified the original pre-deletion development staging commits and easily assembled the fragmented challenge flag!


5. Misc Challenge 2: Waya Dazai khsna nl9aw stage (Stego + OSINT + Crypto)

  • Category: Stego / OSINT / Cryptography
  • Difficulty: Hard
  • Vulnerability: Chained COM Marker Extraction, LinkedIn OSINT & ROT47 / AES-CBC Decryption

5.1 Step 1: Steganography (COM Marker Extraction)

We are supplied with a seemingly ordinary image file: olddays1337.jpeg. JPEGs rely on sequential hexadecimal framing markers. While standard image viewing software strictly evaluates image rendering stream boundaries, custom payloads can be injected cleanly inside unparsed comment markers without corrupting visual graphics!

By evaluating the hex structure of olddays1337.jpeg via binwalk and hexadecimal inspection tools, I uncovered a concealed JPEG Comment (COM) marker (FF FE) injected immediately before the Start of Stream (FF DA / SOS) boundary!

The marker contained a 128-character hexadecimal string representing an encrypted ciphertext: ae8021aa5e4357a9d386fe2794003206d3cb0b231ec461cf48f6538f92a7d197c7009d5ba117cc9154ab207f14db634409055923f39ed4f9489f961a9c308272

5.2 Step 2: OSINT (Tracking the Author’s Digital Footprint)

To decrypt this hex sequence, we needed an encryption passphrase. The challenge title and brief provided a critical dialect hint: “Waya Dazai khsna nl9aw STAGE” (Moroccan Darija meaning “Hey Dazai we need to find an internship / stage”).

Following standard OSINT methodologies, I researched the challenge author’s handle (onevilx). Querying Google and cross-referencing competitive tournament leaderboards on CTFtime revealed the researcher’s full legal profile. Knowing that professional student internships (“stage”) are advertised across corporate networking platforms, I investigated the author’s public LinkedIn profile (https://www.linkedin.com/in/onevilx/).

Buried within an experience description entry on LinkedIn stood a suspicious cipher string:

<6Ji =66E04E7=@42=0`bbf

Analyzing the character distribution and ASCII frequency mapping confirmed this string was encoded utilizing ROT47. Running ROT47 substitution decryption instantaneously revealed our valid passphrase: leet_ctflocal_1337\mathbf{leet\_ctflocal\_1337}

5.3 Step 3: Cryptography (Automated AES-CBC Decryption)

The extracted 128-character hex payload represented a 16-byte Initialization Vector (IV) followed by an AES-256-CBC encrypted ciphertext. To break the cipher, we derive our 256-bit cryptographic key by executing a standard SHA-256 digest hashing loop over our OSINT passphrase (leet_ctflocal_1337).

Below is the complete Python production decryption script (exp.py) that I programmed to automatically unpack the cryptographic payload:

#!/usr/bin/env python3
# exp.py -- Waya Dazai AES-CBC Decryption Tool
import hashlib
import binascii
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
# 1. Provide the passphrase successfully harvested via LinkedIn OSINT & ROT47
PASSPHRASE = "leet_ctflocal_1337"
# 2. Derive the 256-bit AES encryption key via SHA-256 hashing
key = hashlib.sha256(PASSPHRASE.encode()).digest()
# 3. Hexadecimal payload extracted directly from olddays1337.jpeg COM marker (FF FE)
extracted_hex = "ae8021aa5e4357a9d386fe2794003206d3cb0b231ec461cf48f6538f92a7d197c7009d5ba117cc9154ab207f14db634409055923f39ed4f9489f961a9c308272"
extracted_bytes = binascii.unhexlify(extracted_hex)
# 4. Separate the 16-byte Initialization Vector (IV) from the core ciphertext
iv = extracted_bytes[:16]
ciphertext = extracted_bytes[16:]
# 5. Initialize AES cipher in CBC mode and decrypt target buffer
cipher = AES.new(key, AES.MODE_CBC, iv)
padded_flag = cipher.decrypt(ciphertext)
# 6. Remove cryptographic block padding and print winning flag
flag = unpad(padded_flag, AES.block_size)
print("[+] Decrypted Winning Flag:", flag.decode())

Executing python3 exp.py directly across our local shell instantly strips the CBC block padding and unmasks the ultimate multi-stage competition flag:

[+] Decrypted Winning Flag: leet{m4st3rm1nd_0s1nt_w1th_st3g_4nd_crypt0}

📂 Explore My CTF & Bug Bounty Repositories

Every single challenge writeup, custom tool, analysis pipeline, and original Python exploit script demonstrated across this publication is archived cleanly within my public GitHub repository:

Special thanks to the 1337 School infrastructure engineering staff for continuously architecting compelling, realistic vulnerability scenarios.