3 Real Bugs I Found: From CTF Flags to a Live Smart Contract Vulnerability
3 Real Bugs I Found: From CTF Flags to a Live Smart Contract Vulnerability I'm a student developer working through a 150-day self-directed ethical hacking roadmap, split between web security fundamentals and smart cont
3 Real Bugs I Found: From CTF Flags to a Live Smart Contract Vulnerability
I'm a student developer working through a 150-day self-directed ethical hacking roadmap, split between web security fundamentals and smart contract auditing (Soroban/Stellar). This post covers three bugs I found along the way — two from CTF challenges, and one from auditing my own smart contract project. Each one taught me something that applies far beyond the specific bug.
Bug #1: Server-Side Template Injection → Full RCE
This one came from a CTF web exploitation challenge called "SSTI1." The target was a Flask app using Jinja2 templates.
Step 1: Confirm the injection
The first move with any suspected SSTI is the classic math probe. If the app reflects user input back into a template, {{7*7}} will render as 49 instead of literally printing {{7*7}}. That confirmed Jinja2 was evaluating my input as a template expression, not just displaying it as text.
Step 2: Escalate to RCE
Once you know Jinja2 is evaluating expressions, the next question is: what can you reach from there? Jinja2 templates run inside Python, and Python objects carry references to other objects through dunder attributes like __class__, __globals__, and __builtins__. Jinja2 sandboxes templates from direct Python execution, but it doesn't block attribute access — which is the whole vulnerability.
The payload that got me a shell:
{{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
Breaking this down:
-
self— refers to the current template context -
__init__.__globals__— walks back to the global namespace of the module, which includes__builtins__ -
__builtins__.__import__('os')— imports theosmodule, bypassing the fact thatoswas never directly exposed to the template -
.popen(cmd).read()— runs an arbitrary shell command and returns the output
From there I had command execution, and the flag was sitting in a file I could just cat.
Why this matters: SSTI is often dismissed as "just XSS with extra steps," but it's not — SSTI runs on the server, in the application's own process, with the application's own permissions. A sandboxed templating engine is only as safe as its weakest attribute path, and __globals__ is a well-known one in Jinja2, Mako, Twig, and others.
Bug #2: A Forgotten API Endpoint Leaked a Secret via Heap Dump
This one came from a different CTF challenge, "head-dump." No obvious vulnerability was visible on the main site — no injection points, no broken auth. The clue was hidden in plain sight: the site's own blog post mentioned API documentation.
Step 1: Find the undocumented endpoint
Following that hint led to /api-docs, a Swagger UI page listing the app's REST API. Most of it was expected stuff — login, user data, standard CRUD routes. But tucked under a "Diagnosing" section was something that should never be exposed publicly: GET /heapdump.
Step 2: Understand what a heap dump actually is
A heap dump is a snapshot of a running Node.js process's memory — every variable, every object, every string currently held in memory at that moment. It's meant for developers debugging memory leaks locally, never for a public-facing endpoint. If the app ever held a secret in memory (an API key, a session token, a password during a login check), it's sitting in that snapshot.
Step 3: Extract the secret
I downloaded the .heapsnapshot file — about 11MB of minified JSON. The naive approach (grepping the file, or loading it into Chrome DevTools' Memory tab) didn't work; the file was corrupted on the first download and needed a re-download from a fresh instance.
Once I had a clean file, standard text search tools choked on a single-line minified JSON blob that large. What worked was pulling the exact byte range around the flag marker directly:
$content = Get-Content <file> -Raw
$index = $content.IndexOf("academy{")
$content.Substring($index, 150)
That found the flag string sitting in memory, exactly where it shouldn't have been.
Why this matters: Debug and diagnostic endpoints are an entire class of vulnerability that doesn't show up in a typical checklist of "common web vulns." They don't look dangerous because they're not designed to take input or return user data directly — but anything that exposes raw process memory is a direct line to whatever secrets the app is handling at runtime. The fix is simple: diagnostic routes like /heapdump, /debug, /metrics should never ship to production without authentication, if they ship at all.
Bug #3: A Live Authorization Bug in My Own Smart Contract
The first two bugs were CTF challenges — built to be found. This one was different: a real authorization vulnerability I found while auditing my own project, ChitChain, a Soroban (Stellar smart contract) implementation of a ROSCA/chit fund protocol.
The setup
ChitChain has a function, update_circle_status(), that's supposed to let the right party transition a chit circle between states (e.g., active → completed). Like most Soroban contracts, authorization is enforced with require_auth() — a call that checks whether a specific address has cryptographically signed off on the transaction.
The bug
The function was calling require_auth() on the wrong party. It checked that entry.admin had authorized the call, when the actual authorization should have been tied to the circle itself (circle.require_auth()). In practice, this meant the permission check wasn't actually gating the action the way it was supposed to — the state transition could fail in ways that broke the circle's logic, because the party the contract trusted to authorize the change wasn't the party that should have had that authority.
Why the tests didn't catch it
This is the part that matters most. The existing test suite used mock_all_auths(), a Soroban testing helper that auto-approves every require_auth() call in a test environment. It's genuinely useful for testing business logic without having to construct real signed authorizations for every single call — but it has a sharp edge: it makes authorization bugs invisible. If every require_auth() call silently succeeds in tests, a test checking the wrong party will still pass, because mock_all_auths() doesn't care who was supposed to authorize — it approves everyone.
I only found the actual bug by tracing the authorization logic by hand against the contract's intended access model, not by running the test suite. The tests were all green the entire time.
The fix
// Before
entry.admin.require_auth();
// After
circle.require_auth();
A one-line fix, but it closes a real gap between "who the contract thinks is allowed to act" and "who the tests think is allowed to act."
Why this matters: mock_all_auths() (and equivalents in other frameworks — any blanket "auto-approve all auth checks" test helper) is a convenience that trades test-writing speed for a blind spot. A green test suite tells you your business logic works under the assumption that authorization is correct — it says nothing about whether the authorization itself is correct. If you're auditing a Soroban contract (or any contract using similar test mocking patterns), grep for every require_auth() call and manually verify which party it's checking against the intended access-control model, independent of what the tests assert.
What These Three Bugs Have in Common
On the surface these are three unrelated bugs — a templating engine, a debug endpoint, and a smart contract authorization check. But they share the same underlying lesson: the vulnerability lives in the gap between what a system is supposed to trust and what it actually trusts.
- SSTI trusts that sandboxing blocks dangerous code paths — it doesn't block attribute access, and that's enough.
- The heap dump endpoint trusts that "internal" diagnostic routes won't be found — they were documented in public API docs.
- The ChitChain bug trusts that a passing test suite means authorization is correct —
mock_all_auths()approves everyone, so it can't catch who should've been checked.
None of these required advanced exploitation. Each one required reading past the surface: tracing __globals__ instead of assuming the sandbox held, reading the API docs instead of assuming debug routes are hidden, and tracing require_auth() by hand instead of trusting green tests.
Where This Fits In My Roadmap
This post covers Day 101-102 (CTF practice) and Day 81 (Soroban security auditing) of my 150-day ethical hacking roadmap, which I'm documenting day by day on GitHub (cypher-security). I'm currently past the 100-day mark, working through smart contract auditing, bug bounty hunting, and building a portfolio of real audit findings — including a full writeup of the ChitChain bug in my security-portfolio repo.
If you're working through something similar — CTFs, smart contract security, or just starting out in AppSec — I'd love to hear what you're working on. Feel free to connect or follow along with the roadmap.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.