root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat web-frameworks-code-review.md
// Offensive

Web Frameworks: Code Review

9 Mar 2024 · 15 min read · root access
Overview — An offensive-path TryHackMe room on white-box testing: auditing an application when you already have its source code, instead of poking at it blindly from the outside. It teaches you to read code the way an attacker does — following the source-to-sink model — and applies that methodology to a real Flask target to find and exploit three vulnerabilities: SQL Injection, SSTI, and Path Traversal.
PlatformTryHackMe
CategoryOffensive — Secure Code Review
DifficultyMedium
RoomWeb Frameworks: Code Review

Part 1 — Methodology

The core skill isn't memorising dangerous functions — it's reading code with intent: not top-to-bottom like a story, but asking «where does user input enter, and where does it eventually get used in a dangerous way?». Once you can answer that quickly for a codebase, finding real bugs stops being guesswork and becomes a repeatable process.

1.1 How to read an unfamiliar codebase

Opening random files wastes hours. The trick is to read in the order a real request flows through the app, so that by the time you reach the code that handles requests you already understand the context around it. Think of searching a building: you check the front door and the directory board before wandering into random rooms.

StepWhat you readWhy this order matters
1README / DocsTells you what the app is supposed to do, so you can recognise when something doesn't fit that purpose.
2Dependency manifest (requirements.txt)A single outdated, pinned library version can be a known CVE — worth checking before reading a single line of app logic.
3Configuration (config.py)Settings like DEBUG = True or a hardcoded secret key change how every bug you find later should be treated.
4RoutingThis is literally the map of everything the outside world can touch.
5Auth middlewareTells you which routes need a login and which don't — huge for prioritising what to test first.
6Database / ModelsShows you what «normal» data access looks like, so anything that breaks that pattern jumps out later.
7Route handlersThe actual deep-dive — by now you already know what's public, what's protected, and what normal queries look like.

In a Python/Flask project specifically: dependencies live in requirements.txt, routes are created with the @app.route decorator, and the debug flag plus secret key conventionally live in config.py.

1.2 The source-to-sink model

This is the single most useful idea in the whole room. Almost every injection bug boils down to two points connected by a broken (or missing) safety check:

  • Source — where untrusted data enters the app. In Flask: request.args (query string), request.form (form data), request.cookies, or request.json.
  • Sink — a function where that data, if unsanitised, causes real damage. Common ones:
    • cursor.execute() with raw string input → SQL Injection
    • subprocess.run(..., shell=True)Command Injection
    • render_template_string() on unvalidated input → SSTI

The workflow is simple in concept: start at a sink and walk backwards through the code until you either (a) find a source with no sanitisation in between — confirmed bug — or (b) hit a point where the data was properly validated or parameterised — safe. In the room's example, request.args.get("name") flows straight into render_template_string() with nothing in between, a textbook SSTI.

1.3 Letting tools do the first pass

Manually reading every line of a large app doesn't scale, so two tools do the heavy lifting before you ever touch a line by hand:

  • grep -rn "pattern" . — a dumb but fast text search. -r searches every file recursively and -n prints the line number of each match, so you instantly get a clickable list of every place a dangerous function name appears.
  • Semgrep — smarter than grep because it understands code structure (via the AST), not just text. It can spot a dangerous pattern even if variable names or formatting differ. Pointing it at a ready-made ruleset uses the --config flag:
semgrep --config p/owasp-top-ten .

This checks the whole codebase against curated OWASP Top 10 rules in one command. The practical approach: grep first for speed, then Semgrep for coverage, and feed whatever both tools flag into the manual source-to-sink trace from 1.2.

1.4 The vulnerability classes in play

ClassWhat actually causes itTypical sinkCWE
SQL InjectionBuilding a query with an f-string or + instead of a parameterised / bound querycursor.execute(f"...{input}...")CWE-89
Command Injectionshell=True hands the whole string to /bin/sh, so shell metacharacters (;, &&, backticks) get interpretedsubprocess.run(cmd, shell=True)CWE-78
SSTIA user-influenced string gets compiled and executed as a template, instead of being passed as inert data into a static templaterender_template_string(input)CWE-1336
Insecure DeserializationRebuilding Python objects from an untrusted byte stream lets a crafted object's __reduce__ method run arbitrary code the instant it's unpickledpickle.loads(data)CWE-502
Path TraversalJoining user input straight into a filesystem path with no bounds-checking lets ../../.. walk outside the intended directorysend_file(os.path.join(DIR, input))CWE-22
IDORFetching a record by a client-supplied ID with no check that the current user actually owns itVault.query.get(item_id) with no ownership filterCWE-639
🔒 Clearance required

This content is Root Access only. Everything else on the site — the free tier, the whole public library — stays open.

See Root Access plans

No account? Create one free then upgrade from your console.