This commit is contained in:
2025-06-25 23:48:35 +08:00
parent 7e7228bcf4
commit 29024b7061
11 changed files with 933 additions and 0 deletions

373
libseat/README.md Normal file
View File

@ -0,0 +1,373 @@
# 某校图书馆预约系统分析
登录界面如图
![登录](https://oss.flt6.top/imgs/202506081117287.png?x-oss-process=style/init)
输入任意账号、密码正确填验证码F12抓包。注意到访问`http://[URL]/rest/auth?answer=bmx8&captchaId=qxtd5hl5h8wz`,返回`{"status":"fail","code":"13","message":"登录失败: 用户名或密码不正确","data":null}`且无其他访问确定为登录API。
观察url参数未发现账号密码请求为GET不存在data。仔细观察请求体注意到headers中有username和password测试证明为账号密码。
![登录API](https://oss.flt6.top/imgs/202506081117743.png?x-oss-process=style/init)
显然账号密码加密尝试直接重放请求失败注意到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`
![启动器](https://oss.flt6.top/imgs/202506081117170.png?x-oss-process=style/init)
```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()
```

BIN
libseat/image-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
libseat/image-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
libseat/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 KiB

116
libseat/inter.py Normal file
View File

@ -0,0 +1,116 @@
# app.py ── 运行python app.py
import re, threading, datetime as dt
from flask import Flask, render_template_string, request, redirect, url_for, flash
app = Flask(__name__)
app.secret_key = "secret"
# ---------- ❶ 你要周期执行的业务函数 ----------
def my_task():
print(f"[{dt.datetime.now():%F %T}] 🎉 my_task 被调用")
# ---------- ❷ 将 “1h / 30min / 45s” 解析为秒 ----------
def parse_interval(expr: str) -> int:
m = re.match(r"^\s*(\d+)\s*(h|hr|hour|m|min|minute|s|sec)\s*$", expr, re.I)
if not m:
raise ValueError("格式不正确——示例: 1h、30min、45s")
n, unit = int(m[1]), m[2].lower()
return n * 3600 if unit.startswith("h") else n * 60 if unit.startswith("m") else n
# ---------- ❸ 简易调度器 (Thread + Event) ----------
class LoopWorker(threading.Thread):
def __init__(self, interval_s: int, fn):
super().__init__(daemon=True)
self.interval_s = interval_s
self.fn = fn
self._stop = threading.Event()
def cancel(self):
self._stop.set()
def run(self):
while not self._stop.wait(self.interval_s):
try:
self.fn()
except Exception as e:
print("任务执行出错:", e)
worker: LoopWorker | None = None # 全局保存当前线程
current_expr: str = "" # 全局保存当前表达式
# ---------- ❹ Tailwind + Alpine 渲染 ----------
PAGE = """
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>周期设置</title>
<script src="https://cdn.tailwindcss.com/3.4.0"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div class="w-full max-w-md p-8 bg-white rounded-2xl shadow-xl space-y-6"> <header class="flex justify-between items-center">
<h1 class="text-2xl font-semibold text-gray-800">周期设置</h1>
</header> {% with msgs = get_flashed_messages(with_categories=true) %}
{% if msgs %}
{% for cat, msg in msgs %}
<div class="px-4 py-2 rounded-lg text-sm {{ 'bg-green-100 text-green-800' if cat=='success' else 'bg-red-100 text-red-800' }}">
<span>{{ msg }}</span>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="post" class="space-y-4">
<input name="interval" required placeholder="1h / 30min / 45s"
class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:ring-2
focus:ring-indigo-400 focus:outline-none"/>
<button class="w-full py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white font-medium">
设置周期
</button>
</form>
<p class="text-xs text-gray-500">
当前任务:<br>
{% if worker %}
{{current_expr}} 触发一次(刷新页面查看最新状态)
{% else %}
未设置
{% endif %}
</p>
</div>
</body>
</html>
"""
@app.route("/", methods=["GET", "POST"])
def index():
global worker, current_expr
if request.method == "POST":
expr = request.form["interval"].strip()
try:
if expr == "0":
# 如果输入为 "0",则取消当前任务
if worker:
worker.cancel()
worker = None
current_expr = ""
flash("✅ 已取消定时任务", "success")
return redirect(url_for("index"))
sec = parse_interval(expr)
# 取消旧线程
if worker:
worker.cancel()
# 启动新线程
worker = LoopWorker(sec, my_task)
worker.start()
current_expr = expr # 保存表达式
print(expr)
flash(f"✅ 已设置:每 {expr} 运行一次 my_task()", "success")
except Exception as e:
flash(f"{e}", "error")
return redirect(url_for("index"))
return render_template_string(PAGE, worker=worker, current_expr=current_expr)
if __name__ == "__main__":
app.run(debug=True)

205
libseat/libseat.py Normal file
View File

@ -0,0 +1,205 @@
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()