【瑞数】维普期刊搜索接口逆向总结_2_获取Cookie

Python爬虫scrapy

共 7895字,需浏览 16分钟

 · 2021-06-14

文章来源:https://blog.csdn.net/qq_35491275/article/details/117390671作者:mkdir700本篇文章为作者授权转载

前文回顾

在【瑞数】维普期刊 JS 逆向详细流程及 4000 字爬虫总结(1)一文中,成功拿到了搜索接口的签名。

本文主要探究 cookie 的获取

  1. 接口签名的生成与获取

  2. cookie 的生成与获取

  3. 基于浏览器环境的爬虫如何部署?

  4. 关于本次瑞数解密的总结

提出问题

一提到 cookie 的获取,第一想法就是简单。通常的流程就是请求一下网页,然后在响应中提取 cookie 即可。

但是在维普期刊这个例子里,并不是这样。先来了解在调试中我所遇到的实际问题。

然后在后文中,我们一一来解决这些问题。

问题 1:Cookie 从何而来?

问题描述

在上文中,我们是从浏览器中直接复制的 cookie

那么这个 cookie 从哪来?

通过抓包可以知道 cookie 中 name 为 GW1gelwM5YZuS 的值是服务端给的(在第二个问题中有解释)。

在返回的所有响应头中,没有发现设置 GW1gelwM5YZuT 的值,那么可以断定这是在本地生成的。

hook 查看 GW1gelwM5YZuT 何时生成

使用 hook 函数在设置 cookie 时进入 debugger 状态

(function(){
var org = document.cookie.__lookupSetter__('cookie');
document.__defineSetter__("cookie",function(cookie){
if(cookie.indexOf('GW1gelwM5YZuT')>-1){
debugger;
}
org = cookie;
return cookie;
});
document.__defineGetter__("cookie",function(){return org;});
})();

hook 流程

  1. 找到第一次加载的 JS 代码或 JS 文件,在第一行代码上断点;

  2. 调式进入暂停后,在 console 键入 hook 函数;

  3. 放行

加载的第一个 js 文件是 leE4DklasHMb.f22c526.js,在第一行打下断点,然后刷新页面

进入断点后,在 console 键入 hook 代码,按 F8 放行

GW1gelwM5YZuT 被设置,成功暂停

查看调用栈,这里的 _$bs 就是 GW1gelwM5YZuT 的值,是从调用方传过来的,继续向上回溯查看。

然后找到这行代码 xx(773, 1),如果继续向上找,这是在首次允许整个 签名代码 时执行的,且只会调用一次。

换句话说,这行代码是在搜索页面加载时执行的。

Tips:如果上面的调试过程,超过了 40 秒左右,这次放行又会立即暂停到我们的 hook 函数内。

继续往上回溯,可以看到这次是 _$VD(733, 10),这和刚才的 xx(773, 1) 有所不同。

继续向上向上查看调用栈,可以看到类似代码,这是设置了定时器。作用就是每隔 50 秒,调用一次 _$XR 方法,即设置一次 GW1gelwM5YZuT 的值。

以上是比较容易发现的触发方式,另外还有事件可以触发 GW1gelwM5YZuT 的生成,有兴趣可以多调试看看。

  1. 页面加载事件;

  2. 定时器事件;

  3. 鼠标点击事件;

问题 2:搜索页面不匹配

问题描述

在上文中,需要拿到搜索页面的源代码才能进行代码注入。

之前是通过手动复制的方式获取,现在则是需要通过 Python 发送请求拿到搜索页面。

当我直接请求搜索页面,返回的代码如下:

返回的结果明显和之前手动复制的不同。

第一状态码不对,正常请求应该是返回 200

第二内容不对,这次请求返回的 html,虽然也有一段混淆的 JS,但是,body 标签内几乎没有什么代码。

抓包分析

打开抓包工具(本文使用的是 Fiddler),并在浏览器匿名窗口访问搜索主页。

第一次请求,状态码确实是 412。

请求搜索页面的流程:

  1. 请求搜索页面,返回 html 页面,状态码为 412;

  2. html 中引入了 JS 文件,则请求这个 JS 文件;

  3. 再次请求搜索页面,请求成功;


接下来,我们分析如何才能获取到正确的搜索页面。

第一次请求并不是一无是处,首先为浏览器设置了 cookie,这个就是 Gw1gelwM5YZuS 的来源。

然后 html 页面中引入了一个 JS 文件

这一点和上文搜索接口签名一致,共同的特点就是引入 JS 文件赋值,通过 html 中的 JS 代码还原字符串代码,然后加载代码并执行

这个 JS 文件暂时放下不管,先分析第二次搜索页面请求。

可以看到,第二次请求搜索页面时,cookie 多了一个 GW1gelwM5YZut,所以首次请求搜索页面,其作用就是生成可访问搜索页面的 cookie

自问自答环节

Q:既然 cookie 都已经生成了,那么带上这个 cookie 在 python 中发送请求,可以吗?

A:不行,经过多次调试,这个 Cookie 仅能用于访问一次搜索页面。

Q:访问搜索页面的 cookie 可以用于搜索接口吗?

A:不行

关于上面问题与回答,是通过大量调试和踩坑"猜"出来。

如何获取“页面 Cookie”

第一个问题中的 Cookie 是用于搜索接口,这次我们需要获取的 Cookie 则是访问搜索页面的**“钥匙”**。

为了区分,这个 Cookie 称为页面 Cookie

页面 Cookie 关键值两个:GW1gelwM5YZuSGW1gelwM5YZuT

**Tips:**我稍稍看了下药监局的瑞数,这个 Cookie 也是有特点的,会以 xxxxS 和 xxxxT 命名。

改改称呼,方便一点

以 S 结尾的,称作 s_cookie

以 T 结尾的,称作 t_cookie

ok,s_cookie 在上一节的抓包分析中得知,这个值由服务端提供,除此之外 s_cookie 可用于搜索接口的请求,这个可以简单调试看看,多次请求 s_cooie 一直是不变的,t_cookie 则是频繁变化。

接下来就让我们一起来解密 t_cookie,这也是本章的核心内容。

打开审查工具,刷新页面,开始调试,发现第一个请求就是状态码 200,但是我们需要的是状态码为 412 的请求。

这是因为跳转导致(这就是故意为之)

我先讲一讲调试思路。

使用 Fiddler 拦截响应,并只允许通过第一次页面的响应和 JS 文件响应。

这个时候打开审查工具,查看 Application 中的 Cookies,可以看到“新鲜出炉”的 Cookie。

拿出来测试看看,没问题。

验证页面 Cookie 是否可用的代码如下

# -*- coding: utf-8 -*-
"""
Created on 2021/5/25 15:25
---------
@summary:
---------
@author: mkdir700
@email: mkdir700@gmail.com
"""

import requests

session = requests.session()
s = input("请输入GW1gelwM5YZuS的值:\r\n")
value = input("请输入GW1gelwM5YZuT的值:\r\n")
session.headers = {
"Host": "qikan.cqvip.com",
"Connection": "keep-alive",
"sec-ch-ua": '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"',
"sec-ch-ua-mobile": "?0",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Cookie": "GW1gelwM5YZuS={}; GW1gelwM5YZuT={}".format(s, value),
"Sec-Fetch-Site": "none",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-User": "?1",
"Sec-Fetch-Dest": "document",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
}
resp = session.get("http://qikan.cqvip.com/Qikan/Search/Advance?from=index")
# print(resp.text)
# print(resp.request.headers)
print(resp.status_code)

如果用这个页面 Cookie 再请求一次就会失败

页面 Cookie 的自动化获取

我的思路如下:

  1. 依赖浏览器环境,将两个 Cookie 生成

  2. 读取 Cookie 即可

这当中存在一个问题,那就是我没法控制请求的次数。

正常的流程是,浏览器打开页面,会让能加载的都加载,能运行的都运行。

而我们的要求是加载一个 html 页面和一个 JS 文件,为了避免页面 Cookie 被使用而导致失效,此时需要停止后续的所有请求。

手动获取 Cookie 是借助了抓包工具拦截响应的功能。那么有没有什么办法达到同样的效果呢?

我首先想到的是 mitmproxy(中间人),作为中间人,我们可以修改内容,也有权决定请求与响应的去留。

使用 mitmproxy 应该是个比较快速的办法,然后我嫌麻烦(其实没有多麻烦, 哈哈哈),放弃了。

我选择的方式是浏览器插件

我将 hook函数 封装为浏览器插件,只要检测到 t_cookie 生成后,就将其赋值给全局变量。

然后使用 window.stop 停止整个网页的加载,保证页面 Cookie 不会被使用。


inject.js hook 函数代码

var code = function () {
var org = document.cookie.__lookupSetter__('cookie');
document.__defineSetter__("cookie", function (cookie) {
if (cookie.indexOf('GW1gelwM5YZuT') > -1) {
var t = cookie.split("=")[1].split(";")[0];
window.t_cookie = t;
console.log(t);
window.stop();
}
return org;
});
document.__defineGetter__("cookie", function () {
return org;
});
}
var script = document.createElement('script');
script.textContent = '(' + code + ')()';
(document.head || document.documentElement).appendChild(script);
script.parentNode.removeChild(script);

manifest.json

{
"name": "Injection",
"version": "2.0",
"description": "Cookie钩子",
"manifest_version": 2,
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"inject.js"
],
"all_frames": true,
"permissions": [
"tabs"
],
"run_at": "document_start"
}
]
}

这两个文件在同一目录下,制作 chrome 插件的教程,网上搜一搜就行,非常简单。

实现效果

获取 页面Cookie 代码,插件 inject 文件夹与这个 py 文件在同一目录

# -*- coding: utf-8 -*-
"""
Created on 2021/5/25 19:22
---------
@summary:
---------
@author: mkdir700
@email: mkdir700@gmail.com
"""

import asyncio
import os
import time

from pyppeteer import launch
from pyppeteer_stealth import stealth


async def close_page(browser):
await browser.close()


async def start():
# 插件文件夹路径
chrome_extension = os.path.join(os.path.abspath('./'), 'inject')
browser = await launch(
{
'headless': False,
'userDataDir': './userDataDir',
'args': [
'--no-sandbox',
'--load-extension={}'.format(chrome_extension),
'--disable-extensions-except={}'.format(chrome_extension),
'--window-size=0,0'
]
}
)
page = await browser.newPage()
await page.setViewport(viewport={'width': 1000, 'height': 800})
await stealth(page)
await page.goto("http://qikan.cqvip.com/Qikan/Search/Advance?from=index")

time.sleep(0.5)
t = await page.evaluate("() => {return t_cookie;}")
cookies = await page.cookies()
s = None
for c in cookies:
if "GW1gelwM5YZuS" == c['name']:
s = c['value']
data = {'s': s, 't': t}
# print(data)
await browser.close()
return data


def get_cookies():
data = asyncio.get_event_loop().run_until_complete(start())
return data

if __name__ == '__main__':
print(get_cookies())

参考文章:Pyppeteer 如何加载 chrome 插件并测试

总结

cookie 的获取到这里就算结束了,cookie 分为两种类型,一种用于搜索页面,另一种是用于搜索接口。

搜索接口的 cookie 可用于页面,页面的 cookie 不能用于搜索

cookie 中的核心关键是这个 t_cookie,这是在本机生成的。关于他详细的生成逻辑,我没有再分析。

我只知道,服务端返回的 s_cookie 影响着 t_cookie 的值,如果有大佬知道其中的生成逻辑,求告知。

这里在额外说下,搜索页面与 cookie 存在某种绑定关系,比如,拿出 A 浏览器返回的搜索页面源代码,复制 B 浏览器中的 cookie,这样请求搜索接口将会失败。

结合第一篇文章,重新梳理整个执行流程如下:

1. 首次访问搜索页面(状态码412),被设置`s_cookie`;
2. 加载搜索页面及JS代码,被设置`t_cookie`;
3. 跳转,再次请求搜索页面(状态码200);
4. 加载页面及JS代码,重设`t_cookie`(可用于搜索接口)
5. 根据接口地址生成签名;
6. 带上`s_cookie``t_cookie`、签名和请求参数,即可请求成功。



浏览 63
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报