最近碰到一个自动登录的需求,初步对几个按钮点击了下发现除了登录密码加密之外,还有一个滑动验证码的判断,发现正好做一些入门的文章作为分享和记录。
0x01 基本信息收集和流程分析
先完成走一遍流程看看相关的请求,已知网站是一个前后端分离的架构开发,所有的请求在api的目录下,登录查看请求流程如下:

经过对每个请求的查看和业务分析,得到如下的流程:
a. /get
接口是拿到验证码的相关内容
为了方便读者查看,我复制到postman中截图。这个接口请求后会得到4个值,分别是一个secretKey
(暂时未知)、一个完整的滑动验证码的背景图片、一个缺口图片和一个token

实际的图片转码出来后的样式参考如下:

b. `/check`接口用于验证滑动结果
验证接口上传了验证码的type(与get接口相同),上传了token(从get接口的返回包中提取)和一个point字段。

从目前的结果来看,point字段显然就是滑动的结果或者过程数据(部份滑块验证码会用到),但是不是明文,需要进行进一步的分析和尝试解密
c. `/login`接口用于登录

在这个接口中,username是明文的,password和验证码的字段均为密文,经过分析,发现验证码字段的密文与/check接口没有关系,不从该接口返回值取得。所以实际上我们应该只需要
处理/get
和`/login`两个接口即可。
备注:做好分析非常重要,其实这个测试我是按照我的理解先解决了验证码识别的接口,然后在处理登录接口时才发现这个接口其实啥用没有…白费时间
0x02 处理滑动验证码
想知道验证码怎么被服务端验证的,那就只能搞清楚这串加密的验证码明文是什么了,直接截图上流程说明:
a. 找到验证码识别逻辑
基于登录接口的参数,找对应的js文件:

找到文件,在对应的疑似的代码处打断点进行重试,看是否能够被断下:

对代码进行解读,这个验证码的参数从上面的const le过来,实际是一个三元判断,当d.value为真的时候拼接d.value和一个固定的常量,然后对一个json的x/y值做拼接,然后去函数H执行,得到我们的一个最终的字符串。

进一步分析,d.value就是/get
接口里面的secrectKey, r值对应的是我的滑块验证码的距离,或者说缺口在背景图上的横轴位置,y固定一个常量不需要我们处理。接下来就值需要看H函数做了什么:

H函数的逻辑就非常简单了,两个变量进来,f为一个常量,l是一个明文,然后用f做key,进行AES的ECB加密,填充方式为PKCS7,会看对应的H的函数,我们可以简化逻辑:

对应的加密逻辑就是:secretKey拼接一个常量,然后拼接滑块验证码的x/y的json文本,基于secreKey进行AES-ECB加密。
b. 尝试自动识别验证码
得到逻辑之后就可以开始进行自动化的操作了,验证码的识别可以参考我之前的文章:https://blog.mrtblogs.net/burpsuite-ocr-captcha,ddddocr是一个非常好的开源的验证码自动识别库,根据项目说明他也能够支持到简单的滑块验证码的识别。
考虑到我们的项目中服务端没有对滑块的滑动轨迹进行校验,那么我们只需要识别滑块的正确缺口的位置即可。同时考虑到我希望通过n8n完整这个自动化的登录流程,那么我们在本地启动一个FastAPI的服务用于处理验证码识别的请求即可。参考代码:
import base64
import ddddocr
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
# 定义请求体的数据模型
class SlideMatchRequest(BaseModel):
origin: str # Base64 encoded original image
target: str # Base64 encoded jigsaw image
app = FastAPI()
# 初始化 ddddocr,det和ocr都设置为False,只用于滑块匹配
# 注意:ddddocr的初始化可以在应用启动时进行,避免每次请求都重新加载模型
# 为了简单起见,这里放在了请求处理函数内部,如果并发量大,建议移到外面
# det_ocr = ddddocr.DdddOcr(det=False, ocr=False) # 更好的做法是这样初始化一次
@app.get("/")
def read_root():
return {"message": "Slide match API is running"}
@app.post("/slide_match/")
async def perform_slide_match(request: SlideMatchRequest):
"""
接收base64编码的原始图片和滑块图片,返回滑块匹配结果。
"""
try:
# 解码base64图片
target_bytes = base64.b64decode(request.target)
full_bytes = base64.b64decode(request.origin)
# 初始化 ddddocr (如果上面注释的方式初始化了,这里直接使用 det_ocr)
det = ddddocr.DdddOcr(det=False, ocr=False)
# 执行滑块匹配
res = det.slide_match(target_bytes, full_bytes)
return {"result": res}
except base64.binascii.Error:
raise HTTPException(status_code=400, detail="Invalid base64 string provided.")
except Exception as e:
# 捕获其他可能的错误,如ddddocr处理失败
raise HTTPException(status_code=500, detail=f"An error occurred during slide match: {e}")
ddddocr完成识别后,会给出这个缺口对应在完整的验证码图片上的位置,我们取矩形的左边线并适当增加1-5px即可符合要求。
对应到具体的n8n的流程,首先拿到对应的验证码信息,base64的验证码图片:

然后去到本地的FastAPI启动的ddddocr内做识别,拿到缺口的x轴信息:

取到x轴的信息,按照逆向的js加密逻辑使用code节点对我们这次的验证码信息进行加密,作为后续使用:

0x03 解决password加密
和验证码的逆向类似,在同一个文件中找到对应的疑似password加密的地方进行断点测试,发现qe函数:

按F11单步跟踪往后看,最后发现实际是对密码进行了一次RSA的加密,并拿到了公钥:

最终在n8n中实现对密码的加密步骤:

发表回复