← Back to Portfolio CTF Writeup
TryHackMe

Capture!

Web Brute Force Python Scripting
TargetLogin form protected by a math-based CAPTCHA
VulnerabilityUsername enumeration + brute-forceable login
SeverityMEDIUM
ReferencesCWE-307 · CWE-200 · OWASP A07:2021
ToolsPython (requests, lxml)
Flag7df2eabce36f02ca8ed7f237f77ea416

1. Reconnaissance

The target presents a single login form. Two fields request a username and password, but a third field demands the answer to a randomly generated arithmetic expression — a math-based CAPTCHA — on every request. The CAPTCHA value rotates with each page load, which is precisely the control intended to defeat automated attacks.

Submitting a few requests by hand surfaces the first real weakness. The application returns different error messages depending on whether the username exists. A non-existent account and an existing account with a wrong password produce distinguishable responses. This is a classic username enumeration vulnerability.

2. Why Off-the-Shelf Tools Fail

The instinct is to point hydra at the form. It does not work. Hydra and similar tools submit static parameter sets — they cannot read, parse, and solve the rotating CAPTCHA expression embedded in the page on each attempt. The adaptive CAPTCHA invalidates any pre-baked request.

The form must be defeated by a client that behaves like a browser: fetch the page, extract the live CAPTCHA, compute its answer, and submit all three fields together in a single coherent request.

3. Username Enumeration

Using the distinguishable error responses, a wordlist of candidate usernames is fed through a Python loop. For each candidate the script reads the response body and checks which error class was returned, confirming valid accounts before any password guessing begins.

import requests from lxml import html def captcha_answer(page): tree = html.fromstring(page) # CAPTCHA prompt rendered as "What is X + Y?" expr = tree.xpath('//label[@for="captcha"]/text()')[0] a, op, b = parse(expr) # extract operands + operator return str(a + b) if op == '+' else str(a - b) def attempt(user, pwd): s = requests.Session() page = s.get(URL).text data = { 'username': user, 'password': pwd, 'captcha': captcha_answer(page), } return s.post(URL, data=data).text

A critical gotcha: the response text contained HTML-encoded apostrophes ('). Matching error strings against the rendered text silently failed until the comparison was done against the raw HTTP body. Always inspect raw HTTP, never rendered text.

4. CAPTCHA-Aware Password Brute Force

Once a valid username is confirmed, the same machinery drives the password attack. Each iteration fetches a fresh page, solves the new CAPTCHA expression programmatically, and submits the guess. Because the script regenerates the CAPTCHA answer every request, the rate-limiting control is rendered ineffective.

StepActionOutcome
EnumerateLoop usernames, classify error messagesValid account identified
Parselxml extracts the live CAPTCHA expressionArithmetic answer computed
SubmitPOST username + password + CAPTCHA togetherOne valid login attempt per request
IterateRepeat across password wordlistCorrect credentials recovered

5. Flag

With valid credentials the login succeeds and the application returns the flag.

[+] valid username found [+] password matched FLAG: 7df2eabce36f02ca8ed7f237f77ea416
Key Takeaways
Attacker Perspective

Differential error messages reveal whether a username exists before the password attempt. CAPTCHAs that appear in the HTML response body can be solved programmatically — always inspect raw HTTP, not rendered output.

Defender / Remediation

Use identical error messages regardless of whether username or password is wrong. Implement server-side CAPTCHA validation with a CAPTCHA that cannot be solved by parsing the response body. Add rate limiting and account lockout.