Vuln99 merges two prior HackOps Academy projects into one app:
- Glacier's
vulnerable_test_app.py— a lean Flask testbed with detection signatures purpose-built to match Glacier's active scanner (active/scanner.py): reflected XSS, error-based SQLi, boolean-blind SQLi, time-blind SQLi, path traversal, command injection (reflected + blind), and open redirect all use the exact same route paths, parameter names, and error-message format Glacier'scheck_*functions already look for. - VulnMart — a fuller e-commerce app (users, cart, orders, reviews, admin panel) with a per-page difficulty system (low/medium/hard).
Vuln99 keeps Glacier's scanner-compatible signatures, VulnMart's realistic storefront breadth, and generalizes the difficulty system to be per vulnerability (24 of them) rather than per page, configurable live from an in-app admin settings screen.
📖 Learn Vuln99 like a professional: the full course is live at hackops-academy.github.io/vuln99 — 13 modules mapping all 24 vulnerabilities to specific Glacier commands, plus a full combined Glacier × Vuln99 methodology. It's the companion to Glacier's own course — read them together.
This app contains real, working SQL injection, OS command injection,
XXE, SSRF, SSTI, and insecure deserialization. It binds to 127.0.0.1
(localhost) by default, and should stay that way unless you're on an
isolated lab VM with no route to the internet or a shared/corporate
network. See Network binding
below before you ever change that.
- Python 3.9+
pipandvenv(both ship with a standard Python install)- macOS, Linux, or Windows (PowerShell)
git clone https://github.com/hackops-academy/vuln99.git
cd vuln99The fastest path is the setup script — it creates a venv, installs dependencies, and starts the app in one command:
./setup.sh # macOS / Linux.\setup.ps1 # Windows PowerShellOr set it up by hand, if you'd rather see each step:
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
python run.pyGo to http://127.0.0.1:5099 in your browser.
The database is SQLite and reseeds from scratch every time the process starts — every launch boots into the same known, clean state, so you never have to manually reset it between practice sessions.
A pytest smoke suite boots the app and hits every route at every difficulty tier to catch a broken install before you blame the scanner for a bad target:
./setup.sh --test # macOS/Linux
.\setup.ps1 -Test # Windows
# or, with the venv already active:
pip install -r requirements-dev.txt
pytest -qThis does not verify the vulnerabilities themselves — that's the app's whole point — just that nothing 500s.
| Username | Password | Role |
|---|---|---|
| admin | admin123 | admin |
| john_doe | password123 | user |
| alice_smith | password123 | user |
| bob_jones | password123 | user |
Every vulnerability can be set independently via Admin → Difficulty
settings (/admin/settings) — including bulk "set all to low/medium/hard"
buttons — or by editing rows directly in the settings table:
- low — fully vulnerable, no mitigation (Glacier's scanner is tuned to detect these).
- medium — a partial, commonly-seen-in-the-wild mitigation that's
still bypassable (blacklist filtering, naive escaping, rate-limiting
mistaken for access control, etc.) — good for practicing filter/WAF
bypass techniques. Every one of the 24 vulns has a genuinely
bypassable medium tier — see
VULNERABILITIES.mdfor exact payloads. - hard — the properly fixed version (parameterized queries, allow-lists, real session-based auth checks, CSP-safe output encoding).
To reach the admin panel/settings without a valid login when
broken_admin is set to low, visit /admin/become-admin-demo (sets
the forgeable is_admin cookie the low-difficulty check trusts).
For attack payloads and step-by-step walkthroughs for every row below, see
VULNERABILITIES.md.
| Key | Vulnerability | Where |
|---|---|---|
login_sqli |
SQL injection auth bypass | /login |
search_sqli |
UNION-based SQL injection | /search |
product_sqli |
Error-based SQL injection | /product |
blind_sqli_bool |
Boolean-blind SQL injection | /blind-product |
blind_sqli_time |
Time-blind SQL injection | /slow-product |
recommend_sqli |
Cookie-based SQL injection | /recommend |
header_sqli |
HTTP header SQL injection | /category (sink: /admin/logs) |
reflected_xss |
Reflected XSS | /search |
stored_xss |
Stored XSS | product reviews (/product, /reviews) |
csrf |
CSRF | /checkout |
idor_orders |
IDOR | /orders, /account?id= |
broken_admin |
Broken access control | /admin, /admin/settings |
mass_assignment |
Mass assignment | /account (POST) |
price_tamper |
Business-logic / client-trusted price | /cart |
path_traversal |
Path traversal / LFI | /read-file |
file_upload |
Unrestricted file upload | /account/avatar |
command_injection |
OS command injection | /ping, /ping-slow |
open_redirect |
Open redirect | /go |
ssrf |
Server-Side Request Forgery | /admin/import-image |
xxe |
XML External Entity injection | /admin/import-orders |
insecure_deserialization |
Insecure deserialization (pickle) | "Remember me" on /login |
ssti |
Server-Side Template Injection | /admin/email-preview |
weak_crypto |
Weak password hashing (unsalted MD5) | /register |
verbose_errors |
Verbose errors / info disclosure | /product, /version |
Every vulnerable route has a /safe-* sibling where one exists
(/safe-search, /safe-product, /safe-read-file, /safe-ping,
/safe-go) showing the correctly-guarded implementation side by side —
useful for confirming a scanner correctly stays quiet on hardened
endpoints.
/admin shows live stats (users, orders, reviews, activity-log
entries, and a low/medium/hard breakdown of every vuln's current
setting) plus quick links to:
- Difficulty settings (
/admin/settings) — per-vuln control + bulk set-all buttons. - Activity logs (
/admin/logs) — the sink forheader_sqli: shows the last 100 page visits, including rawUser-Agent/X-Forwarded-Forvalues, exactly as they were written to the database. - The existing SSRF/XXE/SSTI admin-only lab tools.
All runtime config is environment-driven (see vuln99/config.py), with
safe defaults if you don't set anything:
| Variable | Default | Purpose |
|---|---|---|
VULN99_HOST |
127.0.0.1 |
Bind address |
VULN99_PORT |
5099 |
Bind port |
VULN99_DEBUG |
on iff host is localhost | Flask debug/reloader |
VULN99_SECRET_KEY |
dev default | Flask session signing key |
VULN99_LOG_DIR |
./logs |
Where access/event logs are written |
VULN99_LOG_LEVEL |
INFO |
Log verbosity |
By default vuln99 only listens on 127.0.0.1. If you're running it in
an isolated lab VM or private lab network and want to point real tools
(nmap, Wireshark, Burp, a scanner running on a different VM in the
same isolated network) at it, override the bind address:
VULN99_HOST=0.0.0.0 VULN99_PORT=5099 python run.pyrun.py will print a warning banner whenever it's bound to anything
other than localhost, as a reminder that this is now reachable from
elsewhere on that network. Only do this on a host that has no route to
the internet or a shared/corporate network — this app has real,
working exploits with no additional sandboxing.
Since vuln99 is a normal HTTP service, it doesn't need anything special
to be scanned/sniffed once it's reachable — nmap -sV -p 5099 <host>
will fingerprint it as a normal Werkzeug/Flask dev server, and any
packet capture on that interface will see plain HTTP traffic you can
correlate against logs/access.log.
Every request is logged to logs/access.log (method, path, status,
response time, source IP, user-agent) and admin actions (difficulty
changes, bulk resets) go to logs/events.log — both rotate at ~2MB.
Handy for lining up what the app saw against a simultaneous nmap scan or
Wireshark capture during a class or CTF run.
run.py entry point
setup.sh / setup.ps1 one-command install + run (+ --test)
requirements.txt runtime deps
requirements-dev.txt + pytest
VULNERABILITIES.md per-vuln attack payload reference
tests/
test_smoke.py route/difficulty-tier smoke suite
vuln99/
__init__.py app factory, blueprint registration, logging setup
config.py env-driven host/port/debug/secret/log config
logging_setup.py access + event logging (logs/access.log, logs/events.log)
db.py SQLite connection + schema
seed.py reseeds users/products/reviews/orders/logs on boot
difficulty.py vulnerability catalog (24) + get/set difficulty
chrome.py shared page shell (nav/footer/CSS)
helpers.py session helpers + IDOR enumeration guard
sample_files/ documents for the path-traversal lab
routes/ one file per feature area — see repo for full list
Point Glacier's active
scanner at http://127.0.0.1:5099/ with the broken_admin/injection-family
vulnerabilities set to low (the default) — the route paths, parameter
names (username/password, q, id, file, host, url), and SQL
error format (sqlite3.OperationalError) match what active/scanner.py's
check_* functions already expect, so existing Glacier scans against the
old vulnerable_test_app.py continue to work unmodified against Vuln99.
For the full walkthrough — every one of the 24 vulnerabilities mapped to a specific Glacier module, plus a combined methodology — see Vuln99's course, written as the companion to Glacier's course.
MIT License — see LICENSE for details.
Built by HackOps Academy — the live-fire range for Glacier.
