374 lines
12 KiB
Markdown
374 lines
12 KiB
Markdown
# 某校图书馆预约系统分析
|
||
|
||
登录界面如图
|
||

|
||
|
||
输入任意账号、密码,正确填验证码,F12抓包。注意到访问`http://[URL]/rest/auth?answer=bmx8&captchaId=qxtd5hl5h8wz`,返回`{"status":"fail","code":"13","message":"登录失败: 用户名或密码不正确","data":null}`且无其他访问,确定为登录API。
|
||
观察url参数,未发现账号密码;请求为GET不存在data。仔细观察请求体,注意到headers中有username和password,测试证明为账号密码。
|
||

|
||
显然账号密码加密,尝试直接重放请求失败,注意到headers中的`x-hmac-request-key`等,推测存在加密。
|
||
|
||
## headers中的`X-hmac-request-key`等参数
|
||
|
||
全局搜索关键字`x-hmac-request-key`,找到唯一代码
|
||
```javascript
|
||
{
|
||
var t = "";
|
||
t = "post" === e.method ? g("post") : g("get"),
|
||
e.headers = c()({}, e.headers, {
|
||
Authorization: sessionStorageProxy.getItem("token"),
|
||
"X-request-id": t.id,
|
||
"X-request-date": t.date,
|
||
"X-hmac-request-key": t.requestKey
|
||
})
|
||
}
|
||
```
|
||
动态调试找到`g`的实现
|
||
```
|
||
function g(e) {
|
||
var t = function() {
|
||
for (var e = [], t = 0; t < 36; t++)
|
||
e[t] = "0123456789abcdef".substr(Math.floor(16 * Math.random()), 1);
|
||
return e[14] = "4",
|
||
e[19] = "0123456789abcdef".substr(3 & e[19] | 8, 1),
|
||
e[8] = e[13] = e[18] = e[23] = "-",
|
||
e.join("")
|
||
}()
|
||
, n = (new Date).getTime()
|
||
, r = "seat::" + t + "::" + n + "::" + e.toUpperCase()
|
||
, o = p.a.decrypt(h.default.prototype.$NUMCODE);
|
||
return {
|
||
id: t,
|
||
date: n,
|
||
requestKey: m.HmacSHA256(r, o).toString()
|
||
}
|
||
}
|
||
```
|
||
|
||
注意到`o = p.a.decrypt(h.default.prototype.$NUMCODE);`未知,确定`h.default.prototype.$NUMCODE`是常量,且`p.a.decrypt`与时间无关,直接动态调试拿到解密结果`o=leos3cr3t`.
|
||
|
||
其他算法实现均未引用其他代码,转写为python
|
||
```python
|
||
def generate_uuid():
|
||
hex_digits = '0123456789abcdef'
|
||
e = [random.choice(hex_digits) for _ in range(36)]
|
||
e[14] = '4'
|
||
e[19] = hex_digits[(int(e[19], 16) & 0x3) | 0x8]
|
||
for i in [8, 13, 18, 23]:
|
||
e[i] = '-'
|
||
return ''.join(e)
|
||
|
||
def g(e: str):
|
||
uuid = generate_uuid()
|
||
timestamp = int(time.time() * 1000)
|
||
r = f"seat::{uuid}::{timestamp}::{e.upper()}"
|
||
secret_key = b"leos3cr3t"
|
||
hmac_obj = hmac.new(secret_key, r.encode('utf-8'), hashlib.sha256)
|
||
request_key = hmac_obj.hexdigest()
|
||
return {
|
||
"id": uuid,
|
||
"date": timestamp,
|
||
"requestKey": request_key
|
||
}
|
||
```
|
||
|
||
至此,使用上述`g`动态生成hmac后,其他内容不变条件下成功重放请求,可以拿到登录token。
|
||
|
||
注意到账号密码均加密,尝试逆向。
|
||
|
||
## 账号密码加密
|
||
|
||
从`http://[URL]/rest/auth?answer=bmx8&captchaId=qxtd5hl5h8wz`请求的启动器中找到关键函数`handleLogin`
|
||

|
||
```javascript
|
||
handleLogin: function() {
|
||
var t = this;
|
||
if ("" == this.form.username || "" == this.form.password)
|
||
return this.$alert(this.$t("placeholder.accountInfo"), this.$t("common.tips"), {
|
||
confirmButtonText: this.$t("common.confirm"),
|
||
type: "warning",
|
||
callback: function(t) {}
|
||
});
|
||
this.loginLoading = !0,
|
||
Object(m.y)({
|
||
username: d(this.form.username) + "_encrypt",
|
||
password: d(this.form.password) + "_encrypt",
|
||
answer: this.form.answer,
|
||
captchaId: this.captchaId
|
||
}).then(function(s) {
|
||
"success" == s.data.status ? (sessionStorageProxy.setItem("loginType", "login"),
|
||
sessionStorageProxy.setItem("token", s.data.data.token),
|
||
t.loginLoading = !1,
|
||
t.getUserInfoFunc()) : (t.$warning(s.data.message),
|
||
t.refreshCode(),
|
||
t.clearLoginForm(),
|
||
t.loginLoading = !1)
|
||
}).catch(function(s) {
|
||
console.log("出错了:", s),
|
||
t.$error(t.$t("common.networkError")),
|
||
t.loginLoading = !1
|
||
})
|
||
},
|
||
```
|
||
注意到
|
||
```javascript
|
||
username: d(this.form.username) + "_encrypt",
|
||
password: d(this.form.password) + "_encrypt",
|
||
```
|
||
动态调试找到`d`的实现
|
||
```
|
||
var ....
|
||
, l = "server_date_time"
|
||
, u = "client_date_time"
|
||
, d = function(t) {
|
||
var s = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : l
|
||
, e = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : u
|
||
, a = r.a.enc.Utf8.parse(e)
|
||
, i = r.a.enc.Utf8.parse(s)
|
||
, n = r.a.enc.Utf8.parse(t);
|
||
return r.a.AES.encrypt(n, i, {
|
||
iv: a,
|
||
mode: r.a.mode.CBC,
|
||
padding: r.a.pad.Pkcs7
|
||
}).toString()
|
||
}
|
||
```
|
||
显然`arguments.length`恒为1,故`s=l="server_date_time"`,`e=u="client_date_time"`,至此AES加密的参数均已知,python实现该AES加密后验证使用的AES为标准AES加密。
|
||
故python实现账号密码的加密:
|
||
```python
|
||
def encrypt(t, s="server_date_time", e="client_date_time"):
|
||
key = s.encode('utf-8')
|
||
iv = e.encode('utf-8')
|
||
data = t.encode('utf-8')
|
||
|
||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||
ct_bytes = cipher.encrypt(pad(data, AES.block_size)) # Pkcs7 padding
|
||
ct_base64 = b64encode(ct_bytes).decode('utf-8')
|
||
return ct_base64+"_encrypt"
|
||
```
|
||
|
||
关于验证码,`http://[URL]/auth/createCaptcha`返回验证码base64,提交时为URL参数,均未加密。
|
||
至此,完成整个登录过程的逆向。
|
||
|
||
## 其他API
|
||
|
||
所有其他API请求headers均带有`X-hmac-request-key`,逻辑同上。URL参数中的token为登录API返回值。
|
||
|
||
## 完整demo
|
||
|
||
这是一个座位检查demo,查询某个区域是否有余座,如有则通过`serverchan_sdk`发生通知。
|
||
需按照实际情况修改常量。
|
||
为了识别验证码,调用了某验证码识别API,该平台为收费API,与本人无关,不对此负责,如有违规烦请告知。
|
||
|
||
Github账号出问题了只能直接贴代码了
|
||
|
||
```python
|
||
import time
|
||
import random
|
||
import hmac
|
||
import hashlib
|
||
import requests
|
||
from datetime import datetime
|
||
import json
|
||
from serverchan_sdk import sc_send;
|
||
|
||
|
||
from Crypto.Cipher import AES
|
||
from Crypto.Util.Padding import pad
|
||
from base64 import b64encode
|
||
|
||
URL = "http://[your libseat url]"
|
||
UID = "[uid]" # 图书馆账号
|
||
PWD = "[password]" # 图书馆密码
|
||
USERNAME = "[username]" # 验证码平台用户名
|
||
PASSWORD = "[password]" # 验证码平台密码
|
||
TOKEN = "[token]"
|
||
|
||
def encrypt(t, s="server_date_time", e="client_date_time"):
|
||
key = s.encode('utf-8')
|
||
iv = e.encode('utf-8')
|
||
data = t.encode('utf-8')
|
||
|
||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||
ct_bytes = cipher.encrypt(pad(data, AES.block_size)) # Pkcs7 padding
|
||
ct_base64 = b64encode(ct_bytes).decode('utf-8')
|
||
return ct_base64+"_encrypt"
|
||
|
||
def b64_api(username, password, b64, ID):
|
||
data = {"username": username, "password": password, "ID": ID, "b64": b64, "version": "3.1.1"}
|
||
data_json = json.dumps(data)
|
||
result = json.loads(requests.post("http://www.fdyscloud.com.cn/tuling/predict", data=data_json).text)
|
||
return result
|
||
|
||
def recapture():
|
||
res = requests.get(URL+"/auth/createCaptcha")
|
||
ret = res.json()
|
||
im = ret["captchaImage"][21:]
|
||
result = b64_api(username=USERNAME, password=PASSWORD, b64=im, ID="04897896")
|
||
return ret["captchaId"],result["data"]["result"]
|
||
|
||
def login(username,password):
|
||
captchaId, ans = recapture()
|
||
url = URL+"/rest/auth"
|
||
parm = {
|
||
"answer": ans.lower(),
|
||
"captchaId": captchaId,
|
||
}
|
||
headers = build_head("post", None)
|
||
headers.update({
|
||
"Username": encrypt(username),
|
||
"Password": encrypt(password),
|
||
"Logintype": "PC"
|
||
})
|
||
res = requests.post(url, headers=headers, params=parm)
|
||
# print("Status:", res.status_code)
|
||
ret = res.json()
|
||
# print("Response:", ret)
|
||
if ret["status"] == "fail":
|
||
return None
|
||
else:
|
||
return ret["data"]["token"]
|
||
|
||
def generate_uuid():
|
||
hex_digits = '0123456789abcdef'
|
||
e = [random.choice(hex_digits) for _ in range(36)]
|
||
e[14] = '4'
|
||
e[19] = hex_digits[(int(e[19], 16) & 0x3) | 0x8]
|
||
for i in [8, 13, 18, 23]:
|
||
e[i] = '-'
|
||
return ''.join(e)
|
||
|
||
def g(e: str):
|
||
uuid = generate_uuid()
|
||
timestamp = int(time.time() * 1000)
|
||
r = f"seat::{uuid}::{timestamp}::{e.upper()}"
|
||
secret_key = b"leos3cr3t"
|
||
hmac_obj = hmac.new(secret_key, r.encode('utf-8'), hashlib.sha256)
|
||
request_key = hmac_obj.hexdigest()
|
||
return {
|
||
"id": uuid,
|
||
"date": timestamp,
|
||
"requestKey": request_key
|
||
}
|
||
|
||
def build_head(e: str,token:str):
|
||
sig = g(e)
|
||
headers = {
|
||
"Authorization": token,
|
||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||
"X-hmac-request-key": sig["requestKey"],
|
||
"X-request-date": str(sig["date"]),
|
||
"X-request-id": sig["id"]
|
||
}
|
||
if token is None:
|
||
headers.pop("Authorization")
|
||
return headers
|
||
|
||
def get_floor_data(token: str,buildingId= "1"):
|
||
date = datetime.now().strftime("%Y-%m-%d") # 获取当前日期
|
||
url = f"{URL}/rest/v2/room/stats2/{buildingId}/{date}"
|
||
params = {
|
||
"buildingId": buildingId,
|
||
"date": date,
|
||
"token": token
|
||
}
|
||
headers = build_head("get",token)
|
||
|
||
response = requests.get(url, headers=headers, params=params)
|
||
|
||
print("Status:", response.status_code)
|
||
res = response.json()
|
||
if res["status"] != "success":
|
||
print("Error:", res)
|
||
return -1
|
||
else:
|
||
ret = []
|
||
for room in res["data"]:
|
||
ret.append({"roomId": room["roomId"], "room": room["room"], "free": room["free"], "inUse": room["inUse"], "totalSeats": room["totalSeats"]})
|
||
# print(f"Room ID: {room['roomId']}, Name: {room['room']}, free: {room['free']}, inUse: {room['inUse']}, total: {room['totalSeats']}")
|
||
return ret
|
||
|
||
def get_room_data(token: str,id:str = "10"):
|
||
date = datetime.now().strftime("%Y-%m-%d") # 获取当前日期
|
||
# 替换为目标接口地址
|
||
print(date)
|
||
url = f"{URL}/rest/v2/room/layoutByDate/{id}/{date}"
|
||
params = {
|
||
"id": id,
|
||
"date": date,
|
||
"token": token
|
||
}
|
||
|
||
sig = g("get")
|
||
headers = {
|
||
"Authorization": token,
|
||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||
"X-hmac-request-key": sig["requestKey"],
|
||
"X-request-date": str(sig["date"]),
|
||
"X-request-id": sig["id"]
|
||
}
|
||
|
||
response = requests.get(url, headers=headers, params=params)
|
||
|
||
print("Status:", response.status_code)
|
||
ret = response.json()
|
||
if ret["status"] != "success":
|
||
print("Error:", ret)
|
||
return -1
|
||
else:
|
||
print("Data:", ret["data"]["name"])
|
||
print("Response:", )
|
||
|
||
def send_message(msg):
|
||
print(sc_send(TOKEN, "图书馆座位", msg))
|
||
|
||
def main(dep=0):
|
||
if dep>3:
|
||
print("无法获取数据!")
|
||
send_message("无法获取数据!")
|
||
return
|
||
years = [2021, 2022, 2023, 2024]
|
||
house = []
|
||
classes = [1,2,3]
|
||
num = range(1,21)
|
||
token = None
|
||
try:
|
||
print("正在尝试登录...")
|
||
tried =0
|
||
while token is None and tried <= 5:
|
||
id = UID
|
||
# id = str(random.choice(years)) + random.choice(house) + str(random.choice(classes)) + str(random.choice(num)).zfill(2)
|
||
token = login(id,PWD)
|
||
if token is None:
|
||
print("登陆失败:",token)
|
||
time.sleep(random.randint(10, 30))
|
||
tried += 1
|
||
if token is None:
|
||
print("登录失败,请检查账号密码或网络连接。")
|
||
main(dep+1)
|
||
return
|
||
print("登录成功,Token:", token)
|
||
data = get_floor_data(token, "1") # 获取一楼数据
|
||
if data == -1:
|
||
print("获取数据失败,请检查网络连接或Token是否有效。")
|
||
main(dep+1)
|
||
else:
|
||
ret = []
|
||
for room in data:
|
||
if room["free"] > 0:
|
||
ret.append(room)
|
||
if ret:
|
||
msg = "|区域|空座|占用率|\n|-|-|-|\n"
|
||
for room in ret:
|
||
msg += f"|{room['room']}|{room['free']}|{1-room['free']/room['totalSeats']:0.2f}|\n"
|
||
send_message(msg)
|
||
except Exception as e:
|
||
print("发生错误:", e)
|
||
main(dep+1)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|
||
```
|
||
|