本文转自FreeBuf.COM
站点数据已脱敏,使用 xxx 表示 本文基于黑箱分析,部分内容非有效动作,仅作为排错以及验证过程展示
** **
目标数据获取流程(用户态)
用户登录
https://xxx/plat/login?synAccessSource=h5&loginFrom=h5&type=logout
跳转到主界面
https://xxx/plat/shouyeUser
然后点击付款
https://xxx/plat/pay?nodeId=15&loginFrom=h5
要获取的数据为40778129653692506121
即 查看数字 那部分内容 而条形码与二维码的解析内容也是这串数字
过程分析
数据包分析
(已过滤资源与 js 文件) 其中标红数据包(id 37,68)初步判断为关键有效项 下面分别分析
数据包 id:37
认证包,POST 请求携带用户名与密码,返回包含关键参数access_token
数据包 id:68
websocket 连接请求 仅有单向通讯,客户端向服务器发消息,传输数据为
其中userId
内容正是我们需要的支付码
推测支付码为客户端本地生成(可能是将用户 id 与时间进行加密后作为支付码),再发送给服务器进行登记使用
代码审阅
由于网页 js 文件进行了压缩,仅有单行,不方便调试,于是先保存到本地。
文件结构
├─ 付款码.html
└─ 付款码_files
app.892a663e.css
app.b45024b7.js.下载
cardpay.svg
chunk-0a3f7412.5241d975.js.下载
chunk-0b9fab22.d78e378c.js.下载
chunk-10eb720c.7887c8cc.css
chunk-10eb720c.cb614830.js.下载
chunk-1bfe6064.69249a75.css
chunk-1bfe6064.9ebc95bb.js.下载
chunk-2d0cf502.d3b50d1b.js.下载
chunk-2d21a7f5.caf0d7aa.js.下载
chunk-2d21d0c2.b51727b1.js.下载
chunk-2d22673a.f40a8a36.js.下载
chunk-2d22a11c.9403ebcb.js.下载
chunk-4518e542.6dba3aad.js.下载
chunk-4cc56ecd.865baaef.css
chunk-4cc56ecd.dec2ba16.js.下载
chunk-5dde23d5.3ff654ee.js.下载
chunk-5dde23d5.c5454cce.css
chunk-76ee7552.59423d06.js.下载
chunk-fa993c88.7cdb37fb.css
chunk-fa993c88.cf44569d.js.下载
chunk-fe09330e.143bcc46.css
chunk-fe09330e.2ac3e7c4.js.下载
chunk-vendors.4c0400c5.css
chunk-vendors.95895f96.js.下载
loading.gif
paycode.000ea068.js.下载
Edge 浏览器Ctrl+S保存,发现依赖项文件夹 付款码_files 内的 js 文件都增加了.下载
后缀来防止误执行
使用 cmd 命令(PowerShell 不可用)去除.下载
后缀
ren _.js.下载 _.
数据溯源
根据 WebSocket 内容首先在文件夹内搜索userId
,寻找数据源头
排查到引用了this.qrcode
变量(chunk-fa993c88.cf44569d.js L1936) 继续溯源搜索qrcode
qrcode
有 4 处赋值语句,而这 4 处处理流程基本相似,下面对其一处(chunk-fa993c88.cf44569d.js L1795)进行分析
进入判断后 o = this.codes.barcode
然后 L1777 进行一次过滤,L1783 删除首元素,如果仍有元素,则将首元素赋值给this.qrcode
(L1795)
网页调试
为验证猜想,L1767 与 L1801 打断点debugger
var e = this;debugger;
}, 300));debugger;
Fiddler 开启 AutoResponder 用本地修改过的文件替换网页原先的 chunk-fa993c88.cf44569d.js 文件(注意浏览器 DevTools 开启禁用缓存)
可惜刷新页面后,并没有停止在断点处,可能是没有通过此处进行赋值, 于是继续测试之前 4 处赋值语句中的其他位置
(已加入调试语句)
campusCardAfter
函数内断点会在每次页面刷新时调用,
能够看到a
中存放的正是我们想要的支付码,其数据源是this.codes.barcode
继续探查barcode
变量,发现并没有明显的赋值过程(有也只是类似先复制出一个barcode
对象,然后进行处理,再重新赋值给barcode
)
但是找到这部分代码(chunk-fa993c88.cf44569d.js L1708-L1710)
codes: localStorage.getItem("barcode")
? JSON.parse(localStorage.getItem("barcode"))
: [],
发现本地存储中存有barcode
变量,里面存放着支付码信息。(实际上应该先清空localStorage
,再进行测试)
localStorage.barcode = [
{
expires: 172800,
barcode: [
"40556116680367244682",
"40427500396723912935",
"40532966131530106739",
"40616042672184885167",
"40292904039749786708",
"40467887275518089314",
"40090871415248895341",
"40274724330740455508",
"40026998262309530846",
"40074626173082122138",
"40289421604037124807",
"40020004343507194680",
"40990309021750279243",
"40336601800307009738",
"40752092092809061117",
"40018889369138192361",
"40525889113799032635",
"40844062976732763809",
"40775401545052749823",
"40946320320688595353",
],
time: 1692168476261,
icon: "cardpay",
id: 1,
},
];
在读取本地存储前打断点,然后删除存储后继续运行(或者删除后强制刷新页面) 发现出现了一个新的网络包,在之前的抓包中未出现过,正是我们需要的支付码数据包。
脚本实现
由上述分析,需要至少发送三个数据包来模拟客户端进行操作
-
1. 认证请求包
-
2. paycode 请求包
-
3. websocket 通讯包
python 实现
代码已脱敏,不可运行,仅供参考
import requests
import json
import time
import websockets
import asyncio
async def send_qrcode(qrcode):
async with websockets.connect(
"wss://xxx/websocket/mobile_service_platform/qrcode_ykt/payCode"
) as ws:
ws_data = {
"appCode": "mobile_service_platform",
"module": "qrcode_ykt",
"userId": qrcode,
}
s = json.dumps(ws_data)
await ws.send(s)
print("send", s)
if __name__ == "__main__":
cookies = ""
# token
url = "https://xxx/berserker-auth/oauth/token"
headers = {
"Connection": "keep-alive",
"Content-Length": "126",
"sec-ch-ua": '"Not/A)Brand";v="99", "Microsoft Edge";v="115", "Chromium";v="115"',
"Content-Type": "application/x-www-form-urlencoded",
"sec-ch-ua-mobile": "?0",
"Authorization": "Basic bW9iaWxlX3NlcnZpY2VfcGxhdGZvcm06bW9iaWxlX3NlcnZpY2VfcGxhdGZvcm1fc2VjcmV0",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.203",
"sec-ch-ua-platform": '"Windows"',
"Accept": "*/*",
"Origin": "https://xxx",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
"Referer": "https://xxx/plat/login?type=logout&type=login&synAccessSource=h5&loginFrom=h5",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
}
data = "username=XXXXXX&password=XXXXXX&grant_type=password&scope=all&loginFrom=h5&logintype=sno&device_token=h5&synAccessSource=h5"
session = requests.session()
session.headers.clear()
session.headers.update(headers)
response = session.post(url=url, data=data, verify=False)
cookies = session.cookies
content = json.loads(response.content)
print("token", content["access_token"])
time.sleep(3)
# qrcode
url1 = "https://xxx/berserker-app/ykt/tsm/batchGetBarCodeGet?account=95633&payacc=%23%23%23&paytype=1&synAccessSource=h5"
headers1 = {
"Connection": "keep-alive",
"sec-ch-ua": '"Not/A)Brand";v="99", "Microsoft Edge";v="115", "Chromium";v="115"',
"synjones-auth": "bearer " + content["access_token"],
"sec-ch-ua-mobile": "?0",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.203",
"sec-ch-ua-platform": '"Windows"',
"Accept": "*/*",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
"Referer": "https://xxx/plat/pay?nodeId=15&loginFrom=h5",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
}
session1 = requests.session()
session1.cookies.update(cookies)
session1.headers.clear()
session1.headers.update(headers1)
response1 = session1.get(url=url1, verify=False)
content1 = json.loads(response1.content)
qrcode = content1["data"]["barcode"][0]
print("qrcode", qrcode)
time.sleep(3)
# websocket
asyncio.run(send_qrcode(qrcode))
js 实现(油猴)
代码已脱敏,不可运行,仅供参考
// ==UserScript==
// @name example.com
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author BrokenClient
// @match *://example.com/*
// @match *://*.example.com/*
// @icon none
// @run-at document-end
// @grant GM_xmlhttpRequest
// @grant GM_info
// @require https://cdn.jsdelivr.net/npm/[email protected]/qrcode.min.js
// @connect xxx
// ==/UserScript==
(function () {
"use strict";
let token = "";
let qrcode = "";
//console.log(GM_info);
//alert(GM_info.version)
async function main() {
token = await get_token();
console.log("token", token);
//alert(token);
qrcode = await get_qrcode(token);
console.log("qrcode", qrcode);
//alert(qrcode);
qrcode_img.makeCode(qrcode);
let ws_data = {
appCode: "mobile_service_platform",
module: "qrcode_ykt",
userId: "40778129653692506121",
};
ws_data["userId"] = qrcode;
let ws = new WebSocket(
"wss://xxx/websocket/mobile_service_platform/qrcode_ykt/payCode"
);
ws.onopen = (params) => {
let s = JSON.stringify(ws_data);
console.log("send", s);
ws.send(s);
//alert(s);
};
console.log("main over");
}
document.getElementsByTagName("div")[0].remove();
let div_qrcode = document.createElement("div");
div_qrcode.id = "qrcode";
div_qrcode.style = "position:absolute;width:80%;height:60wh;top:5rem;";
document.body.appendChild(div_qrcode);
const qrcode_img = new QRCode("qrcode", {
text: "qrcode",
width: 0.8 * window.innerWidth,
height: 0.8 * window.innerWidth,
});
main();
})();
function get_token() {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "POST",
url: "https://xxx/berserker-auth/oauth/token",
headers: {
Connection: "keep-alive",
"Content-Length": "126",
"sec-ch-ua":
'"Not/A)Brand";v="99", "Microsoft Edge";v="115", "Chromium";v="115"',
"Content-Type": "application/x-www-form-urlencoded",
"sec-ch-ua-mobile": "?0",
Authorization:
"Basic bW9iaWxlX3NlcnZpY2VfcGxhdGZvcm06bW9iaWxlX3NlcnZpY2VfcGxhdGZvcm1fc2VjcmV0",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.203",
"sec-ch-ua-platform": '"Windows"',
Accept: "*/*",
Origin: "https://xxx",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
Referer:
"https://xxx/plat/login?type=logout&type=login&synAccessSource=h5&loginFrom=h5",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
},
data: "username=xxxxxx&password=xxxxxx&grant_type=password&scope=all&loginFrom=h5&logintype=sno&device_token=h5&synAccessSource=h5",
timeout: 5000,
onload: (response) => {
let j = JSON.parse(response.responseText);
let token = j["access_token"];
resolve(token);
},
onerror: (e) => {
console.log(e);
},
});
});
}
function get_qrcode(token) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: "https://xxx/berserker-app/ykt/tsm/batchGetBarCodeGet?account=95633&payacc=%23%23%23&paytype=1&synAccessSource=h5",
headers: {
Connection: "keep-live",
"sec-ch-ua":
'"Not/A)Brand";v="99", "Microsoft Edge";v="115", "Chromium";v="115"',
"synjones-auth": "bearer " + token,
"sec-ch-ua-mobile": "?0",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.203",
"sec-ch-ua-platform": '"Windows"',
Accept: "*/*",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
Referer: "https://xxx/plat/pay?nodeId=15&loginFrom=h5",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
},
timeout: 5000,
onload: (response) => {
let j = JSON.parse(response.responseText);
let qrcode = j["data"]["barcode"][0];
resolve(qrcode);
},
onerror: (e) => {
console.log(e);
},
});
});
}
js 效果:
打开 example.com,页面会自动处理,并生成一个可以使用的支付二维码
补充
移动端使用油猴脚本:实测该js脚本不能在Via,X浏览器上使用,其搭载的扩展执行器版本过低,推荐使用狐猴浏览器安装油猴插件管理器来实现脚本运行。
结语
如有建议或优化思路,欢迎评论指正或私信
原文始发于微信公众号(合天网安实验室):某平台登录和支付接口分析与脚本实现
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论