# 爬虫项目6 JS逆向之url参数模拟 **Repository Path**: cthousand/item-6 ## Basic Information - **Project Name**: 爬虫项目6 JS逆向之url参数模拟 - **Description**: 部分网站的url构成中含有加密参数,这些参数的加密方法写在了javascript中,而js通常经过了压缩,混淆和加密; 本项目通过ajax断点,hook方法,playwright等方法模拟加密过程从而实现url正确构造 - **Primary Language**: Python - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2022-05-02 - **Last Updated**: 2022-07-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # js逆向之加密参数破解 ## 背景说明 部分网站的url构成中含有加密参数,这些参数的加密方法写在了javascript中,而js通常经过了压缩,混淆和加密;本项目通过ajax断点,hook方法先寻找到加密入口,其次通过playwright将加密方法挂载到window对象中,达到利用浏览器环境执行javascript方法的目的,从而在不需要知道具体加密逻辑的情况下,模拟参数加密。 ## 目标明确 1. 获取url:https://spa6.scrape.center/,网址中的100条电影数据的标题,评分和简介字段,并保存至mysql中。![image-20220502124458998](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021245778.png) ![image-20220502124614568](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021246606.png) 2.结果 ![image-20220502124746165](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021247202.png) ## 方法分析 ### 列表页url分析 ![image-20220502125016284](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021250319.png) 这是一个ajax请求,token参数是加密参数,limit参数表示每页电影展示数量,offset根据观察,为(当前页数-1)*10; 为了观察token的构造,我们打一个ajax断点,在发出ajax请求的一瞬间停下,用调用栈寻找token的生成入口 ![image-20220502125443135](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021254168.png) 按个查看调用栈,找到token生成入口, ![image-20220502130229811](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021302848.png) 在169行打上断点,刷新网页,并将```_0x2fa7bd['a']```和```this['$store']['state']['url']['index']```添加到watch监控中,![image-20220502130912134](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021309176.png) 容易发现_0x2fa7bd['a']是一个方法,传入的参数是```/api/movie``` 此时有2条路径可以走,1是点击```chunk-4dec7ef0.e4c2b130.js:1```,具体的查看这个方法的构造逻辑,并用python模拟该方法,第二条路是直接将```_0x2fa7bd['a']```挂载到浏览器window对象中,具体选用哪种方法可以由该方法的复杂程度决定,这里我们先点进去查看该方法,![image-20220502131450122](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021314159.png) 在return处打上断点,刷新,察看变量的变化![image-20220502131704235](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021317268.png) 1. 将当前的时间戳和```/api/movie```构造成一个列表 2. 用```,```拼接这个列表形成字符串后,用``SHA1``加密形成40个16进制数 3. 再和时间戳构成成列表,并用,拼接成字符串 4. 将字符串用base64方法加密成64个可打印字符 如果用python实现的话要导入hashlib调用sha1方法,导入base64调用encode方法,还是比较繁琐的,这里我们走挂载window对象方式 1. 下载改加密方法所在的js文件![image-20220502133158922](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021331964.png) 2. vscode打开该文件,在该方法调用后的下一行加上如下一句```window.encrypt = Object(_0x2fa7bd['a']);```意思是将该方法变成window对象的一个属性![](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021333450.png) 3. 调用playwright库改写js加载路径 ```python from playwright.sync_api import sync_playwright browser = sync_playwright().start().chromium.launch() page = browser.new_page() page.route('https://spa6.scrape.center/js/chunk-19c920f8.c3a1129d.js', lambda route: route.fulfill(path='./项目6/chunk.js')) page.route('https://spa6.scrape.center/js/chunk-4dec7ef0.e4c2b130.js', lambda route: route.fulfill(path='./项目6/chunk-id.js')) self.page.goto(self.BASEURL) ``` 4. 于是playwright的浏览器实例便具备了调用该加密方法的能力,紧接着我们写一个方法,让浏览器执行这个一个javascript方法,它的作用是让传入进来的参数,实现这个加密方法并返回输出结果 ```python def get_token(self, params): result = self.page.evaluate( '()=>{return window.encrypt("%s")}' % params) return result ``` 于是列表页的url构造便完成了,接下来用requests/aiohttp/scrapy等方法请求该url即可 ### 详情页url分析 ![image-20220502134530753](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021345796.png) 还是一个ajax请求,有2个加密参数,2个都是64位的字符串,可能都用了base64加密方法,token的构造方法可能和列表页的一样,前面那个,暂且叫id不清楚 1. 先调查token的加密入口,看方法是否与列表页一致,如果一致,看加密参数是什么 ![image-20220502134945074](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021349120.png) ![image-20220502135038514](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021350552.png) ![image-20220502135122949](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021351993.png) 可以看出token的加密方法是一样的,但是加密参数不一样,```/api/movie/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx```,在/api/movie/的基础上还拼接了id,那么接下来方向就在id的加密构造方法上了,但id的构造方法通过查看调用栈的方式并没有找到,所以换一种方法,即然用了base64加密方法,那么很可能是用btoa方法实现的,我们可以借助```tampermonkey```插件写一个简单的JavaScript脚本,进行hook捕获 ```python // ==UserScript== // @name hookbase64 # 脚本名称 // @namespace http://tampermonkey.net/ // @version 0.1 // @description try to take over the world! // @author You // @match https://spa6.scrape.center/ # 目标网址 // @icon https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org // @grant none // ==/UserScript== (function() { 'use strict'; function hook(object,attr){ var func=object[attr] # 先定义一个方法 object[attr]=function(){ # 随后改写这个方法 console.log('hooked',object,attr) # 控制台输出调用的对象和属性 var ret=func.apply(object,arguments) # 调用一开始的方法 console.log('ret',ret) # 控制台输出返回的结果 debugger # 重点:在此处进入调试 return ret # 返回结果 }} hook(window,'btoa') # 实例化这个方法,对象是window,方法是'btao' })(); ``` 取消网页所有断点,刷新 ![image-20220502140927032](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021409076.png) 奇怪的是并没有触发js脚本的运行 可能原因是什么?可能是这个id并不是在这个网页包含的ajax请求中生成的,可能是在列表页网址的ajax请求中就生成了,验证一下 image-20220502141505081 果然debug住了,但这个参数重有时间戳,说明不是这次调用(这里是token的形成过程) 点击```继续执行脚本``` ![image-20220502141632546](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021416592.png) 找到了,参数是一串无意义的字符,通过调用栈,一步步找到它的上层调用方法 ![image-20220502141739076](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021417121.png) 最终我们找到了构造入口,进入查看其加密方法,为2个字符串拼凑后,用base64加密成64位的字符串,一个字符串是固定的,另外一个是传入的参数,而这个参数必然是区别每一步电影之间的唯一标识码,接下来去查看列表页的json文件 ![image-20220502142203700](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021422750.png) 可以推断出ID字段便是这个传入进去的参数 ![image-20220502142715539](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021427582.png) 那么接下来就沿用列表页的方式,将这个方法用playwright挂载到window对象中,构建id生成方法即可 ```python def get_id(self, params): result = self.page.evaluate( '()=>{return window.encrypt_id("%s")}' % params) return result ``` url都构造好后,下面就是水到渠成了。 ### 脚本结构 ![image-20220502144704699](https://cthousand-pic-save.oss-cn-hangzhou.aliyuncs.com/img/202205021447767.png) ## 代码实现 ```python import requests from playwright.sync_api import sync_playwright from loguru import logger import pymysql class Spider(): def __init__(self): # init self.BASEURL = 'https://spa6.scrape.center' self.MAX_PAGE = 10 self.LIMIT = 10 self.browser = sync_playwright().start().chromium.launch() self.page = self.browser.new_page() self.page.route('https://spa6.scrape.center/js/chunk-19c920f8.c3a1129d.js', lambda route: route.fulfill(path='./项目6/chunk.js')) self.page.route('https://spa6.scrape.center/js/chunk-4dec7ef0.e4c2b130.js', lambda route: route.fulfill(path='./项目6/chunk-id.js')) self.page.goto(self.BASEURL) self.INDEX_URL = self.BASEURL + \ '/api/movie?limit={limit}&offset={offset}&token={token}' self.DETAIL_URL = self.BASEURL+'/api/movie/{id}/?token={token}' self.db = pymysql.connect(host='*', user='*', password='*', database='*') self.cursor = self.db.cursor() def get_token(self, params): # todo:token解密 result = self.page.evaluate( '()=>{return window.encrypt("%s")}' % params) return result def get_id(self, params): # todo:id揭秘 result = self.page.evaluate( '()=>{return window.encrypt_id("%s")}' % params) return result def download(self, url): # todo: download_method res = requests.get(url=url) logger.info(res.url) return res def parse_getId(self, json): # todo:解析:拿到id for i in json['results']: id = i['id'] yield id def parse_getData(self, json): # todo:解析>拿到标题,评分,简介 title = json['name'] rating = json['score'] brif = json['drama'] data = [title, rating, brif] logger.info(data) return data def save(self, data): # todo:保存 sql = 'insert into movies(title,rating_num,brif) values(%s,%s,%s)\ on duplicate key update title=%s,rating_num=%s,brif=%s' self.cursor.execute(sql, data*2) self.db.commit() def main(self): # todo:方法调度 for i in range(self.MAX_PAGE): index_url = self.INDEX_URL.format( limit=self.LIMIT, offset=i*10, token=self.get_token('/api/movie')) res = self.download(url=index_url) id = self.parse_getId(res.json()) for i in id: detail_url = self.DETAIL_URL.format(id=self.get_id( i), token=self.get_token('/api/movie/'+self.get_id(i))) res = self.download(url=detail_url) data = self.parse_getData(res.json()) self.save(data) self.db.close() logger.info('over!') if __name__ == "__main__": spider = Spider() spider.main() ```