最近我校突然让我们制作心理测评网站,然后登录中发现有滑块验证码等操作(之前都是直接登录无需验证码,现在界面变的很整洁以及引入了验证码登录)
本人目前初二(八年级)第一次写文章,所以文章可能会有些地方写的不清楚,请谅解!
网站:aHR0cHM6Ly96eXpmeC5wc3l5dW4uY29tLw==
开始分析
验证码获取
验证流程(失败处理):

base64图像示例:
originalImageBase64:

jigsawImageBase64:

可以看到是滑块验证码
这个是比较好解的(ddddocr可以解决)
重点是在请求
接下来是验证成功的请求流程:

解析:
获取验证码:
Get https://zyzfx.psyyun.com/code?userName=[U]
// UserName为用户名
Headers:
Authorization: Basic xxx //Auth必备条件
TENANT-ID: 440 // 租户ID(相当于学校ID)

Authorization固定的值:

TENANT-ID生成方法:
Get https://zyzfx.psyyun.com/admin/tenant/detailByCode?code=zyzfx
// 其中code为学校的代码(网址前缀)
Result:

很好,关键的一些请求头都获取到了
接下来是编写Python代码(获取验证码):
import requests
# Disable SSL Verification
requests.packages.urllib3.disable_warnings()
# Init
session = requests.Session()
session.verify = False
session.headers = {
"Authorization": "Basic xxx", // 获取到的Authorization(可直接写进里面)
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", // 默认请求方式
"TENANT-ID": "440", // 租户ID
}
UserName = "xxx" // 用户名
captcha = session.get(f"https://zyzfx.psyyun.com/code?userName={UserName}&captchaType=blockPuzzle").json() // 获取验证码
imageOriginalBase64 = captcha["data"]["repData"]["originalImageBase64"] // 背景图
imageOriginal = base64.b64decode(imageOriginalBase64)
imagePuzzleBase64 = captcha["data"]["repData"]["jigsawImageBase64"] // 拼图
imagePuzzle = base64.b64decode(imagePuzzleBase64)
token = captcha["data"]["repData"]["token"] // token
secretKey = captcha["data"]["repData"]["secretKey"] // 安全密钥
验证码验证流程
先分析请求

Post请求,内容为空
Query String:
userName: 1 // 用户名
captchaType: blockPuzzle // 验证码类型:滑块验证码
pointJson: c+NDG2sxh8ntCz9nlBvmf3/0T4Z/I1PwkvcC9D2KBlg=
// 加密内容 secretKey:aGvrWRMPxKoeuGMe
token: c62b362137b542fcab00ff073a1932a1 // code中获取到的token
js分析:


解析(pointJson)
使用AES中的ECB模式加密,填充方式为Pkcs7方式,secretKey为加密密钥
在o.a函数中,t为key(获取arguments,获取第二项作为key密钥(没有默认使用密钥:XwKsGlMcdPMEhR1B))
返回内容分析:
失败:
{
"code": 0,
"msg": "成功",
"data": {
"repCode": "6111",
"repMsg": "验证失败",
"repData": null,
"success": false // 验证失败的判断
},
"success": true
}
成功:
{
"code": 0,
"msg": "成功",
"data": {
"repCode": "0000",
"repMsg": null,
"repData": {
"captchaId": null,
"projectCode": null,
"captchaType": "clickWord",
"captchaOriginalPath": null,
"captchaFontType": null,
"captchaFontSize": null,
"secretKey": null,
"originalImageBase64": null,
"point": null,
"jigsawImageBase64": null,
"wordList": null,
"pointList": null,
"pointJson": "c+NDG2sxh8ntCz9nlBvmf3/0T4Z/I1PwkvcC9D2KBlg=", // 请求里面的pointJson
"token": "c62b362137b542fcab00ff073a1932a1", // token值
"result": true,
"captchaVerification": null,
"clientUid": null,
"ts": null,
"browserInfo": null
},
"success": true // 验证成功的判断
},
"success": true
}
这个请求更像是在验证是否正确滑到指定位置(无其他返回参数)
登录部分

Query String:
randomStr: blockPuzzle // 默认滑块验证码
code: grILnYI7HmW41fTsrP7O/WrHm3qTRbHLt0Rq1KVrvLzdfKCSGGKWwGLnuGB6OHfqN5/1jFY457Zrz+LFL0MGG3RE5ka9Y04xQKpe06cmkOs= // 特殊加密的code,其里面包含了获取验证码的token与PointJson的内容
grant_type: password // 粗略推测应该是使用密码类型登录
username: 1 //用户名
POST内容(表单数据):
username: 1 // 用户名
password: Hgn8K/2pda3c5rEnoZIcpA== // 特殊加密的密码
Headers:
isToken: false // 默认为false
TENANT-ID: 440 // 前文提到的学校ID
Authorization: Bearer xxx // 前文提到的Authorization验证部分
Content-Type: application/x-www-form-urlencoded;charset=utf-8 // 默认请求内容的方式:form格式
js代码分析:


先看到key值的部分:

var i = "r"
, r = "u"
, a = "i"
, o = "g"
, c = "e";
function s() {
return "".concat(i).concat(r).concat(a).concat(o).concat(c).concat(i).concat(r).concat(a).concat(o).concat(c).concat(i).concat(r).concat(a).concat(o).concat(c).concat(c)
// 使用拼接的方式拼凑出key,即为ruigeruigeruigee
}

通过这段代码可得知密码使用的是AES CBC的模式加密(nopadding)且key和iv均为固定密钥:ruigeruigeruigee
接下来是Code部分
我们回到刚刚验证验证码验证成功后的部分

captchaVerification值是code的证据:

总结:password使用AES CBC nopadding加密,其iv和key均为ruigeruigeruigee
code为AES ECB Pkcs7加密,密钥为secretKey,内容为:
---
俩个加密的解决了,接下来是编写python代码逻辑:
def Login(UserName,Password):
while True:
# Get Captcha
session.headers.update({"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"})
session.headers.update({"Authorization": "Basic xxx"})
captcha = session.get(f"https://zyzfx.psyyun.com/code?userName={UserName}&captchaType=blockPuzzle").json()
imageOriginalBase64 = captcha["data"]["repData"]["originalImageBase64"]
imageOriginal = base64.b64decode(imageOriginalBase64)
imagePuzzleBase64 = captcha["data"]["repData"]["jigsawImageBase64"]
imagePuzzle = base64.b64decode(imagePuzzleBase64)
token = captcha["data"]["repData"]["token"]
secretKey = captcha["data"]["repData"]["secretKey"]
# DDDOCR
res = ocr.slide_match(imagePuzzle, imageOriginal, simple_target=True)
# Encrypt pointJson
# AES ECB Encrypt
originalJsonData = "{" + f"\"x\":{res['target'][0]}.27272727272727,\"y\":5" + "}"
cipher = AES.new(secretKey.encode(), AES.MODE_ECB)
encrypted_data = cipher.encrypt(pad(originalJsonData.encode(), AES.block_size))
encrypted_code = cipher.encrypt(pad((token + "---" + originalJsonData).encode(), AES.block_size))
encryptedCodeBase64 = base64.b64encode(encrypted_code).decode().replace(" ","+")
encrypted = base64.b64encode(encrypted_data).decode()
encrypted = encrypted.replace(" ", "+")
# Submit Captcha
buildPostJson = {
"userName": UserName,
"captchaType": "blockPuzzle",
"pointJson": encrypted,
"token": token
}
#Json To QueryString
import urllib.parse
queryString = ""
for key, value in buildPostJson.items():
# 对查询参数进行URL编码以避免特殊字符问题
encoded_value = urllib.parse.quote(str(value), safe='')
queryString += f"{key}={encoded_value}&"
queryString = queryString[:-1]
captchaResult = session.post(f"https://zyzfx.psyyun.com/code/check?{queryString}").json()
if captchaResult["data"]["success"] == True:
# # AES CBC Encrypt Password
encrypted_password_b64 = aes_cbc_zero_padding_encrypt(Password, "ruigeruigeruigee")
# Login
# 将URL参数转换为JSON格式再编码为查询字符串
params = {
"randomStr": "blockPuzzle",
"code": encryptedCodeBase64,
"grant_type": "password",
"username": UserName
}
# 手动构建查询字符串,确保特殊字符被正确编码
query_string = "&".join([f"{k}={urllib.parse.quote(str(v), safe='')}" for k, v in params.items()])
url = f"https://zyzfx.psyyun.com/auth/oauth/token?{query_string}"
session.headers.update({"Content-Type":"application/x-www-form-urlencoded","ignore":"1","origin":"https://zyzfx.psyyun.com"})
response = session.post(url, data={"username":UserName,"password": encrypted_password_b64})
loginResult = response.json()
print(f"登录成功,用户名:{loginResult['user_info']['username']}")
session.headers.update({"Authorization":f"Bearer {response.json()['access_token']}"})
break
返回内容:
登录失败:
{"code":1,"msg":"登录失败,用户名或密码错误","data":"unauthorized"}
登录成功:

其中需要添加请求头Authorization: Bearer access_token内容
添加这个请求头后即可请求接下来的如获取心理测评表等API操作
之后就是答题部分(抓包即可抓下来),Python代码(自动完成题目):
DefaultSelectIndex = 0 # 默认选择选项
Login("UserName","Password") // 用户名与密码
# 心理测评表获取(空代表完成)
getSacleTable = session.get("https://zyzfx.psyyun.com/plantest/userPlan/getMyUpscomingScaleTabByPage?source=2&type=1¤t=1&size=10").json()
if getSacleTable["data"]["records"] != []:
print(f"正在测评: {i['studentName']}")
for j in getSacleTable["data"]["records"]:
print(f"测评名称: {j['planTitle']}")
print(f"测评ID: {j['id']}")
# Get Test List
getTestList = session.get("https://zyzfx.psyyun.com/plantest/userPlan/getMyPlanTestScaleByPageAndPlanId?planId=3329").json()
for k in getTestList["data"]:
print(f"测评名称:{k['otherName']}")
print(f"测评ID: {k['testId']}")
print(f"ScaleID: {k['scaleId']}")
getQuestionCount = int(k["questionCount"])
page = getQuestionCount / 10
print("开始答题")
CommitList = []
for m in range(int(page + 1)):
getQuestionList = session.get(f"https://zyzfx.psyyun.com/scale/question/getMyQuestionAndOptionByscaleId?scaleId={k['scaleId']}&testId={k['testId']}&pushId=0¤t={str(m)}").json()
for n in getQuestionList["data"]["records"]:
getDefaultAnswer = n["optionList"][DefaultSelectIndex]
CommitList.append({"questionId":int(getDefaultAnswer["questionId"]),"answerId":int(getDefaultAnswer["id"]),"answerContent":getDefaultAnswer["answerContent"],"answerType":1,"score":int(getDefaultAnswer["score"])})
buildCommitJson = {
"scaleId": int(k["scaleId"]),
"testId": int(k["testId"]),
"studentId": 0,
"useTime": 450,
"isFinish": True,#m == page - 1,
"current": int(CommitList[-1]["questionId"]),
"pushId": 0,
"answerList": CommitList,
"visitorId": None
}
# Commit
session.headers.update({"Content-Type":"application/json"})
commitResult = session.post("https://zyzfx.psyyun.com/plantest/userPlan/commitAnswer",json=buildCommitJson).json()
print(f"提交结果: {commitResult}")
print(f"Complete: {k['otherName']}")
效果展示
