一、前言
碍于一直抢不到某些优惠券,在海鲜市场里面搜索过后发现有人出同款,且只要登录cookie就可以,猜测应该是已经有人开了,那么今天进行一下前端逆向工程实战和讲解,希望对想学但是一直看不懂的同学有所帮助。
本次教程以某网站活动为例。
地址:https://cube.meituan.com/cube/block/ec547f375b92/290373/index.html
二、抓包
首先因为这个页面大概只能在app或者小程序上看到,我们要进行抓包。抓包的方式有很多种,Wireshark,fiddler和手机上面的Stream(iOS)。这个页面在实战中还是比较好抓的,但是瞬时请求量很大,所以看的时间比较久。经过选择Stream中的里面的GET与POST请求(排除掉图片之类的),我们得到了上面的网址。

请求类型:POST
HTTP请求有多种类型,如GET、POST、PUT、DELETE等。POST请求通常用于发送数据到服务器
URL:https://cube.meituan.com/topcube/api/toc/playWay/preSendCoupon
URL是网络资源的地址。在这个例子中,它指向某网站的一个API端点,用于预检优惠券
参数:
请求参数是附加在URL后面,用于向服务器传递额外的信息。
k=290373
yodaReady=h5
csecplatform=4
csecversion=2.4.0
mtgsig
:包含加密的签名信息
请求头:
请求头包含关于请求或响应的元信息,如客户端信息、内容类型、授权信息等。关键的请求头包括:
Host: cube.meituan.com
Accept: */*
Sec-Fetch-Site: same-origin
Accept-Language: en-GB,en;q=0.9
Accept-Encoding: gzip, deflate, br
Sec-Fetch-Mode: cors
Content-Type: application/json
Origin: https://cube.meituan.com
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 TitansX/20.27.2 KNB/1.0 iOS/17.4 meituangroup/com.meituan.imeituan/12.20.403 meituangroup/12.20.403 App/10110/12.20.403 iPhone/iPhone13 WKWebView
Referer: https://cube.meituan.com/cube/block/ec547f375b92/290373/index.html
Content-Length: 1016
Connection: keep-alive
Sec-Fetch-Dest: empty
Cookie
: 各种跟踪和会话管理相关的 Cookie
请求体:
请求体包含实际发送到服务器的数据。在这个例子中,请求体是一个JSON格式的数据,包括:
playWaySecrets:加密的秘密信息。
sourceType:来源类型。
userId:用户ID。
其他参数如requestTime、nonceRandom、requestSign等,用于确保请求的合法性和防止重放攻击。
{
"playWaySecrets": "40c06a5853",
"sourceType": "MEI_TUAN",
"userId": "",
"requestTime": ,
"nonceRandom": "eb3480e2-fca6-e769-d95b-cb24dec3cb82",
"checkGetBeforeTime": false,
"requestSign": "",
"cubeActivityUrl": "",
"cubeActivityId": 290373
}
响应内容(Response)
状态码:200 OK
表示请求成功
响应头:包含关于响应的元信息
Server: openresty
Date: Wed, 15 May 2024 11:34:00 GMT
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
M-TraceId: -28587130892389450
Access-Control-Allow-Origin: https://cube.meituan.com
Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept,__skcy, mtgsig
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET,POST,OPTIONS,DELETE
Content-Encoding: gzip
响应体:服务器返回的实际数据。在这个例子中,响应体是一个JSON格式的数据,包含优惠券的信息和状态,如:
code:响应状态码,0表示成功。
msg:响应消息。
data:包含具体的优惠券信息,如是否有库存、优惠券类型、数量等。
安全性
签名认证:mtgsig和requestSign是用于签名认证的参数,确保请求的真实性和防止篡改。
Cookie:用于会话管理和身份验证,确保每个请求都能正确识别用户。
三、预检代码实现
只是抓到包这就结束了吗?这样就可以重放请求让我们获得优惠券了吗?让我们去网站上面看一下。打开F12,进入源代码,挑一个比较有特色的请求参数进行搜索,以playWaySecrets为例子,看看JS里面都写了些什么:
注:这里可能有些难以实现,有时候清除cookie,调整网速,还有及时中断加载都很重要
哦~虽然源代码经过了webpack加密,但是我们仍然看到了这个请求的完整结构。与此同时,源代码旁边还有这样一条:
可见预检操作只是用来查询库存,而真正的获取优惠券是后面的sendCoupon请求,这也符合我们的代码规范。
不过做了这么久,虽然还没有到达我们抢到优惠券的目标,还是做一点东西满足以下我们的成就感吧!
让我们尝试重放请求:
重放这个请求的Python代码可以使用 requests
库,它是一个用于发送HTTP请求的简单而强大的库。下面是一个示例代码:
import requests
import json
url = "https://cube.meituan.com/topcube/api/toc/playWay/preSendCoupon"
headers = {
"Host": "cube.meituan.com",
"Accept": "*/*",
"Sec-Fetch-Site": "same-origin",
"Accept-Language": "en-GB,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Sec-Fetch-Mode": "cors",
"Content-Type": "application/json",
"Origin": "https://cube.meituan.com",
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 TitansX/20.27.2 KNB/1.0 iOS/17.4 meituangroup/com.meituan.imeituan/12.20.403 meituangroup/12.20.403 App/10110/12.20.403 iPhone/iPhone13 WKWebView",
"Referer": "https://cube.meituan.com/cube/block/ec547f375b92/290373/index.html",
"Content-Length": "1016",
"Connection": "keep-alive",
"Sec-Fetch-Dest": "empty",
"Cookie": ""
}
data = {
"playWaySecrets": "40c06a5853",
"sourceType": "MEI_TUAN",
"userId": "",
"requestTime": ,
"nonceRandom": "eb3480e2-fca6-e769-d95b-cb24dec3cb82",
"checkGetBeforeTime": False,
"requestSign": "",
"cubeActivityUrl": "",
"cubeActivityId": 290373
}
response = requests.post(url, headers=headers, data=json.dumps(data))
print(response.status_code)
print(response.json())
解释:
-
导入库:首先导入
requests
和json
库。 -
URL:定义请求的URL。
-
请求头:在
headers
字典中定义请求头,包括Host
、Accept
、User-Agent
、Content-Type
等。 -
请求体:在
data
字典中定义请求体,包含需要发送的数据。 -
发送请求:使用
requests.post
方法发送POST请求,传入URL、请求头和请求体。 -
处理响应:打印响应的状态码和响应内容。
这个代码示例展示了如何使用Python和requests
库重放一个HTTP POST请求,包括设置请求头和请求体,并处理响应。
那么结果就是:
{'code': 0, 'msg': '返回成功!', 'data': [{'code': 'NOT_START', 'msg': '未开始', 'playWaySecret': '40c06a5853', 'playWayId': 109774, 'restCount': 15, 'totalCount': 15, 'playWayStartTime': 1715774400000, 'title': None, 'businessCouponType': 1, 'money': 0.0, 'minConsume': 0.0, 'discount': 0.0, 'maxMinus': 0.0, 'extend': None, 'couponIcon': None, 'bgConfig': '{"buName":"住宿","buCode":"hotel"}', 'channelUrlKey': '', 'playWayEndTime': 1715788799000, 'couponType': 0, 'playWayTitle': '境外五周年-五折券 ', 'couponValidityStartTime': 0, 'couponValidityEndTime': 0, 'couponUseDeadlineTime': 0}]}
200
{'code': 0, 'msg': '返回成功!', 'data': [{'code': 'NO_STOCK', 'msg': '今日券已发完', 'playWaySecret': '40c06a5853', 'playWayId': 109774, 'restCount': 0, 'totalCount': 15, 'playWayStartTime': 1715774400000, 'title': None, 'businessCouponType': 1, 'money': 0.0, 'minConsume': 0.0, 'discount': 0.0, 'maxMinus': 0.0, 'extend': None, 'couponIcon': None, 'bgConfig': '{"buName":"住宿","buCode":"hotel"}', 'channelUrlKey': '', 'playWayEndTime': 1715788799000, 'couponType': 0, 'playWayTitle': '境外五周年-五折券 ', 'couponValidityStartTime': 0, 'couponValidityEndTime': 0, 'couponUseDeadlineTime': 0}]}
200
非常令人欣慰的结果对吧!对于整个流程,我们已经完成了一半了。
四、发送代码实现
我们先观察一下这部分的代码:
这段代码是一个用JavaScript编写的函数,用于处理红包的领取逻辑。它包括了事件监听、数据准备、签名生成、请求发送和响应处理。为了方便理解,我将代码拆分成几个部分并逐一解释。
代码整体结构
-
函数定义和事件监听:
key: "takeRedpacket", value: function(l) { var f = this; m.default.on(function(s) {
takeRedpacket
是一个对象的键,值是一个函数,该函数接收参数l
。m.default.on
用于监听一个事件,当事件触发时,执行回调函数。
-
数据准备:
(0, y.default)(function(c, e, t) { var a, d = t || "", u = e || window.Block.UUID || ""; c ? (0, b.default)((a = (0, n.default)(p.default.mark(function e(t) {
y.default
可能是一个工具函数,用于异步执行操作。c
是用户ID,e
是UUID,t
是token。- 检查
c
是否存在,如果存在则继续执行。
-
准备请求数据:
var n, a, o, r, i; return p.default.wrap(function(e) { for (; ; ) switch (e.prev = e.next) { case 0: return a = s.server, o = k.default.guid(), n = { positionCityId: l.data.positionCityId, uuid: u, visitCityId: l.data.visitCityId, playWaySecrets: l.data.id, userId: c, sourceType: l.data.platform, weixinCode: "", voucherSourceType: h.default.native ? "INNER_APP" : "OUTER_APP", requestTime: a, nonceRandom: o, requestSign: btoa("playWaySign," + btoa(a + "," + o)), mini_program_token: d, cubeActivityUrl: window.location.href, cubeActivityId: window.Block.ID },
a
是服务器时间,o
是一个随机生成的GUID。n
是请求数据对象,包含了多个字段,如positionCityId
、uuid
、visitCityId
、playWaySecrets
、userId
等。requestSign
是请求签名,使用btoa
进行Base64编码。
-
添加H5指纹和请求签名:
h.default.business && (n.ecube_sid = d), a = JSON.parse(t), e.next = 7, f.getFingerPrint(); case 7: return a.h5_fingerprint = e.sent, n.RiskForm = btoa((0, g.default)(a)), o = { contentType: l.type, contentEncoding: "" }, e.next = 12, f.addRequestSignature("POST", T, v.default.queryStringify(n), o);
- 如果
h.default.business
存在,则设置ecube_sid
。 - 调用
f.getFingerPrint
获取H5指纹,并添加到请求数据中。 - 将
n
转换成查询字符串,并调用f.addRequestSignature
添加请求签名。
- 如果
-
发送请求并处理响应:
case 12: r = e.sent, i = w.default.post(r.sigUrl).withCredentials().set("Content-Type", l.type), (i = r.mtgsig ? i.set("mtgsig", r.mtgsig) : i).send(n).end(function(a, o) { v.default.log({ api: r.sigUrl, request: n, response: a ? a.toString() : o }), a ? (0, x.default)(r.sigUrl, r.mtgsig, n, function(t, e) { t ? (KNB.getNetworkType({ success: function(e) { v.default.raptorLog({ name: "sendCouponError-removesign", msg: "\u591a\u72b6\u6001\u7ea2\u5305\u53d1\u5238\u9519\u8bef" }, (0, g.default)({ err: a.toString(), xhrErr: t, sigUrl: r.sigUrl, status: i.xhr ? i.xhr.status : "\u6ca1\u6709xhr", readyState: i.xhr ? i.xhr.readyState : "\u6ca1\u6709xhr", statusText: i.xhr ? i.xhr.statusText : "\u6ca1\u6709xhr", responseText: i.xhr ? i.xhr.responseText : "\u6ca1\u6709xhr", responseType: i.xhr ? i.xhr.responseType : "\u6ca1\u6709xhr", networkType: e.networkType })) } }), l.callback(a, o)) : (window.Block.trigger("$blockComponentRedpacketRefresh", !0), l.callback(t, { body: e })) }) : (o && o.body && (2032 === o.body.code || 99999 === o.body.code) && v.default.raptorLog({ name: "addRequestSignature", msg: "KNB\u9a8c\u7b7e\u5931\u8d25" }, (0, g.default)({ err: a, sigUrl: r.sigUrl })), window.Block.trigger("$blockComponentRedpacketRefresh", !0), l.callback(a, o)) });
- 发送带有签名的POST请求。
- 处理响应并记录日志,如果有错误则进行错误处理和回调。
- 成功时触发
"$blockComponentRedpacketRefresh"
事件,更新页面状态。
要实现这段代码的重放,我们需要获取以下关键信息和数据:
-
用户相关信息:
userId
:用户的唯一标识符(例如用户ID)。uuid
:用户设备的唯一标识符(可能是设备的UUID)。
-
城市相关信息:
positionCityId
:用户当前所在城市的ID。visitCityId
:用户访问的城市ID。
-
活动相关信息:
playWaySecrets
:活动的密钥或标识符。cubeActivityId
:活动的ID。cubeActivityUrl
:活动页面的URL。
-
请求时间和随机数:
requestTime
:请求的时间戳。nonceRandom
:随机生成的字符串(GUID)。
-
签名和指纹:
requestSign
:基于特定规则生成的请求签名。h5_fingerprint
:设备的指纹信息。
-
其他信息:
sourceType
:请求来源类型(例如INNER_APP
或OUTER_APP
)。voucherSourceType
:优惠券来源类型。mini_program_token
:小程序的token(如果适用)。weixinCode
:微信的授权码(如果适用)。RiskForm
:风险评估表单信息(加密后的JSON字符串)。
-
必要的库和函数:
getFingerPrint
:获取设备指纹的函数。addRequestSignature
:生成请求签名的函数。
我们可以在代码里找到对应的函数:
从这段代码来看,获取设备指纹是一个异步操作。在JavaScript中,它使用一个回调函数来获取指纹。
在JavaScript代码中,addRequestSignature 方法调用 c.default.sign 来生成签名,并通过回调函数返回签名的URL和头部信息mtgsig。
然而,事情没有这么简单。这种防御拉满的接口还会调用更多的函数:
t.sign = function(t, n, o, e, d, i) {
if (g)
d();
else {
var a = {
url: t,
method: n,
headers: o,
data: e
};
if (!i || !c.native)
return window.H5guard ? (a.headers = {
"content-encoding": o.contentEncoding,
"content-type": o.contentType
},
void window.H5guard.sign(a).then(function(n) {
d(n)
})) : void u(function(n) {
n ? (a.headers = {
"content-encoding": o.contentEncoding,
"content-type": o.contentType
},
window.H5guard.sign(a).then(function(n) {
d(n)
})) : d(!1)
});
r.ready(function() {
r.addRequestSignature({
method: n,
url: t,
body: e,
header: o,
success: function(n) {
var e = t;
n.mtgsig ? "string" == typeof n.mtgsig ? o.mtgsig = JSON.parse(n.mtgsig).mtgsig : o.mtgsig = n.mtgsig.mtgsig : e = n.url,
d({
url: e,
headers: o
})
},
fail: function() {
d({
url: t
})
}
})
})
}
}
,
t.getFingerPrint = function(e) {
g ? e() : window.H5guard ? e(window.H5guard.getfp()) : u(function(n) {
e(n ? window.H5guard.getfp() : "")
})
}
,
n.exports = t
至此仍未结束。需要的信息和功能列表:
签名服务(window.H5guard.sign):模拟或替代的签名服务,能够根据输入生成签名和签名后的 URL。
指纹服务(window.H5guard.getfp):模拟或替代的指纹服务,能够生成设备指纹。
本地环境检测(c.native 和 window.H5guard):需要确定如何在 Python 环境中模拟或替代这些检测。
回调函数(用于处理签名和指纹结果):在 Python 中,我们可以使用同步函数或 async/await 模式来模拟回调函数。
异步操作处理(u 和 r.ready):需要处理异步操作以确保操作在适当的时机完成。
调用控制台:
我们可以发现里面会牵扯越来越多的函数和变量,但是由于已经混淆了,我们几乎不可能硬解这些数据,所以想个办法,能不能尝试直接从接口或者浏览器获取?
控制台中,window.H5guard.getfp()成功返回了指纹数据:
实现步骤
以下是实现重放这段代码的步骤:
-
准备数据:
- 收集用户、城市和活动相关信息。
- 生成请求时间戳和随机数。
-
生成签名和指纹:
- 调用
getFingerPrint
函数获取设备指纹。 - 调用
addRequestSignature
函数生成请求签名。
- 调用
-
构建请求数据:
- 按照代码中的格式构建请求数据对象。
-
发送请求并处理响应:
- 使用
requests
库发送带有签名的POST请求。 - 处理响应并记录日志。
- 使用
以下是一个Python代码示例,展示了如何实现这段代码的重放:
import requests
import json
import uuid
import time
from base64 import b64encode
def get_fingerprint():
# 假设这里是获取设备指纹的函数
return "example_fingerprint"
def add_request_signature(method, url, query_string, headers):
# 假设这里是生成请求签名的函数
# 返回签名和签名的URL
return {
"sigUrl": url,
"mtgsig": "example_mtgsig"
}
# 准备数据
user_id = "77046512"
uuid = "0000000000000C66B32075291418895F81B0288CBAD8FA167857834046472258"
position_city_id = "118"
visit_city_id = "118"
play_way_secrets = "40c06a5853"
cube_activity_id = 290373
cube_activity_url = "https://cube.meituan.com/cube/block/ec547f375b92/290373/index.html?cube_id=290211&cityId=118&city_id=118"
source_type = "INNER_APP" if False else "OUTER_APP" # 根据实际情况判断
request_time = int(time.time() * 1000)
nonce_random = str(uuid.uuid4())
request_sign = b64encode(b"playWaySign," + b64encode(f"{request_time},{nonce_random}".encode())).decode()
fingerprint = get_fingerprint()
risk_form = b64encode(json.dumps({"h5_fingerprint": fingerprint}).encode()).decode()
# 构建请求数据
data = {
"positionCityId": position_city_id,
"uuid": uuid,
"visitCityId": visit_city_id,
"playWaySecrets": play_way_secrets,
"userId": user_id,
"sourceType": source_type,
"weixinCode": "",
"voucherSourceType": source_type,
"requestTime": request_time,
"nonceRandom": nonce_random,
"requestSign": request_sign,
"mini_program_token": "",
"cubeActivityUrl": cube_activity_url,
"cubeActivityId": cube_activity_id,
"RiskForm": risk_form
}
headers = {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 TitansX/20.27.2 KNB/1.0 iOS/17.4 meituangroup/com.meituan.imeituan/12.20.403 meituangroup/12.20.403 App/10110/12.20.403 iPhone/iPhone13 WKWebView"
}
# 生成请求签名
signature = add_request_signature("POST", "https://cube.meituan.com/topcube/api/toc/playWay/preSendCoupon", json.dumps(data), headers)
# 发送请求
response = requests.post(signature["sigUrl"], headers=headers, data=json.dumps(data))
# 处理响应
print(response.status_code)
print(response.json())
这是个大坑啊。打包完的工程默认开了严格模式,控制台无法注入,还得换个思路
【未完待续】