文泉学堂爬虫:Jwt-HS256加解密详解

武汉加油,中国加油

因新冠状病毒引发肺炎疫情影响,清华大学对外开放知识库,至开学前(2月16日)全国用户可免费免登陆进入“文泉学堂”网站阅读,我们借这个机会,通过文泉了解一下Jwt-HS256的加密与破解。

https://lib-nuanxin.wqxuetang.com/#/

声明

  1. 本文尽仅涉及到 Jwt-HS256 的数据加解密 ,不涉及书籍的下载与爬取;
  2. 本文仅限学习交流使用,不得用于商业非法用途,否则,一切后果请用户自负。
  3. 如有侵权请邮件与我联系处理。

应用程序&依赖

Chrome
python 3.8
PyCharm
PostMan Canary
pip install pyjwt
pip install requests

解密思路及过程

  1. 找到需要解密的数据
  2. 进入阅读书籍页面,通过调试 JavaScript 找到Token的加密方法;
  3. 通过分析加密方法获取破解思路;
  4. 通过破解加密调试 JavaScript ,得到加密前的参数
  5. 解析参数的生成规律,自由生成可用参数
  6. 参数根据原方法加密,获得可用Token

一、找到需要解密的数据

1. 首先打开任意一本书的阅读链接

https://lib-nuanxin.wqxuetang.com/read/pdf/3209130
# 观察到该链接最后为一串数字「3209130」,根据经验猜测这串数字应该是该书的「id 」或是「唯一标识」。

观察到书籍信息是通过图片方式显示的,在向上滚动网站的同时会加载下一页的图片。

2. 通过禁用 JavaScript 来判断网页中哪些内容是动态生成的

chrome://settings/content/javascript?search=javascript
# 在设置中搜索JavaScript或键入以上代码快捷进入JavaScript设置页

十分尴尬的是发现禁用了JavaScript之后,整个网页都无法加载了,由此可知整个网站全部由JavaScript动态渲染,无法通过静态请求拿到书籍信息,于是再次允许JavaScript运行。

扫描二维码关注公众号,回复: 8963633 查看本文章

3.进入Chrome控制台NETWORK选项查看JS加载的内容

在控制台的NETWORK选项卡中看到Img文件格式的书籍图片即为我们要获取的数据
在这里插入图片描述

分析一下该图片的链接:
在这里插入图片描述
结合每页图片的链接格式分析可得:
3209130 对应书籍ID,「1」对应页码,?k=一串加密的Token

格式:
https://lib-nuanxin.wqxuetang.com/page/img/「书籍ID」/ 「页码」?k=「加密后的Token」

直接将图片URL放入Postman中请求,发现并不能直接请求到图片,经过检查发现请求的referer不能为空,添加referer后可以成功请求到图片文件。(图片很大,请求好慢)
在这里插入图片描述

到此为止可知,只要能自由生成可用的图片链接后的Token,并根据「书籍ID」和「页码」拼接成有效的图片URL,即可下载图片。

二、调试JavaScript,获取加密方法

我们在控制台Network中该请求的Initiator属性中可以找到他调用的JavaScript文件和函数。在这里插入图片描述

使用的JS文件及入口函数

- 「read.v5.3.1.722eb.js」 ----- setPageSrc
- 「chunk-vendors.v5.3.1.722eb.js」

在控制台中Source中找到这两个JS文件并点击左下角花括号fromat代码。
搜索入口函数setPageSrc,并在函数结尾设置断点
在这里插入图片描述

重新载入网页,网页暂停在「2050」断点处,同时以小红字展示出该函数的目前的所有临时变量。
在这里插入图片描述

在这里发现类似「token」的临时变量「s」,用控制台输出一下,果然是需要的「Token」

可以看到临时变量「s」是由「this.getJwt」函数生成的。

>var s = i ? "width=100" : "k=" + this.getJwt(this.bid, t);

取消「2050」处断点并搜索「getJwt」函数,在函数结尾设置断点,重新载入。
发现临时变量「a」为我们需要的token
在这里插入图片描述

临时变量「a」由函数「ue.a.sign」生成

var a = ue.a.sign({
                        p: t,
                        t: Date.parse(new Date),
                        b: e,
                        w: 1e3,
                        k: this.$store.state.aesK
                    }, this.$store.state.jwtSecret.toString());

继续向内查找ue.a.sign函数,发现搜索无法搜索到函数体,于是在控制台输入「ue.a.sign」,输出函数体,点击跳转进入ue.a.sign函数体
在这里插入图片描述

发现函数体不在「read.v5.3.1.722eb.js」里面而是进入了「chunk-vendors.v5.3.1.722eb.js」中,函数体很长,我们同样删除原断点后在函数结尾设置断点,重新载入
在这里插入图片描述

这次发现了关键信息:

header: {alg: "HS256", typ: "JWT"}

这个信息告诉我们,该Token是通过「JWT-HS256」加密的。

三、JWT-HS256 加密详解

根据header,我们发现该token的加密模式为「Jwt-HS256」加密。

Jwt 全称表示为:JSON Web Tokens ,官网为https://jwt.io,Jwt-HS256加密由以下三个部分组成:

  • header
  • payload
  • signature

三个部分使用「Base64」编码后拼接组成Token,中间用点分隔,刚刚看到书籍图片链接后面的长长的一串就是加密完成的Token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwIjoxLCJ0IjoxNTgwNzM4ODA4MDAwLCJiIjoiMzIwOTEzMCIsInciOjEwMDAsImsiOiJ7XCJ1XCI6XCI1SklXUG9yaEFGaz1cIixcImlcIjpcInVnVnQ5RC95NHE4Rk9kMFhqeXh0aFE9PVwiLFwidFwiOlwiTFdTcTJDOG54YzY0ZkN4RDM0aVp2dz09XCIsXCJiXCI6XCJaWmxiVnNOUGdWQT1cIixcIm5cIjpcImhJRXJzcnlYUDhVPVwifSIsImlhdCI6MTU4MDczODgwOH0.yyC518F2QiI9AHUMhMLa1YVxudzUedGBsiToELRMl1M

Tips:Base64是一种编码,并不是一种加密过程,不具备安全性,可以任意编码解码。
Tips:由于「+」「/」「=」三个符号在url中有特殊意义,所以在用作token时需要删除。

header

header一般是固定的内容,由使用的算法和Token的类型组成:

{alg: "HS256", typ: "JWT"}
# 使用的算法是HS256 , token的类型为JWT

通过Base64编码后如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload

Payload是Token的具体内容,格式为JSON,内容可以自定。

{
	"name": "ITHOMIA",
	"email": "[email protected]",
	"qq": "3339118668"
}

通过Base64编码后如下:

ewoJIm5hbWUiOiAiSVRIT01JQSIsCgkiZW1haWwiOiAiaXRob21pYUBvdXRsb29rLmNvbSIsCgkicXEiOiAiMzMzOTExODY2OCIKfQ==

删除结尾的等号:

ewoJIm5hbWUiOiAiSVRIT01JQSIsCgkiZW1haWwiOiAiaXRob21pYUBvdXRsb29rLmNvbSIsCgkicXEiOiAiMzMzOTExODY2OCIKfQ

Signature

Signature部分由Base64编码后的header和payload拼接,中间用点分隔 ,之后再加入Secret通过HS256算法加密后得到。

Tips:Secret 是一串由服务端定义的密钥,不应该对客户端展示;
Tips:注意这里的HS256加密和Hmacsh256加密不同,HS256加密后返回密文长度为43,而Hmacsh256返回长度为64。

实例

Jwt在使用需要引用第三方依赖,不推荐自己编写HS256算法,绝大多数第三方依赖无需手动将JSON转化为Base64编码,直接将JSON作为参数传入即可

Python实现

import jwt

header = {"alg": "HS256", "typ": "JWT"}
payload = {"name": "ITHOMIA", "email": "[email protected]", "qq": "3339118668"}
Secret = 'ITHOMIA'
k = jwt.encode(headers=header, payload=payload, key=Secret).decode()
print(k)

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSVRIT01JQSIsImVtYWlsIjoiaXRob21pYUBvdXRsb29rLmNvbSIsInFxIjoiMzMzOTExODY2OCJ9.M_tk6WkRPb4pnHloQxZS8qyK1Hu9wb-oCZ-MwrASC5g

Python中的pyjwt模块内置header固定为 { “typ”: “JWT”,“alg”: “HS256”}, 只能添加无法修改。
在header的typ和alg顺序不同时,会导致Base64编码后的数据不同,具体数据如下。

{ “typ”: “JWT”,“alg”: “HS256”} Base64=> eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
{ “alg”: “HS256”,“typ”: “JWT”} Base64=> eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

  • 解决方法为修改模块的内置方法,详情见我的另一篇博文。1

JavaScript实现

const jsrsasign = require("/usr/local/lib/node_modules/jsrsasign");
var header = {"alg": "HS256", "typ": "JWT"};
var payload = {"name": "ITHOMIA", "email": "[email protected]", "qq": "3339118668"};
var Secret = 'ITHOMIA';

var k = jsrsasign.jws.JWS.sign(null, header, payload, Secret);
console.log(k);

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSVRIT01JQSIsImVtYWlsIjoiaXRob21pYUBvdXRsb29rLmNvbSIsInFxIjoiMzMzOTExODY2OCJ9.M_tk6WkRPb4pnHloQxZS8qyK1Hu9wb-oCZ-MwrASC5g

四、破解加密,获取加密前的参数

获取加密前的参数本应调试JavaScript取得,但我们已知参数的加密方式为Jwt-HS256,同时刚刚提到,Base64是一种编码,并不是一种加密过程,不具备安全性,所以没有必要继续调试JS,只需要将Base64编码的Token转换回字符串,即可得到加密前的参数信息。

将Token明文化

从书籍图片网址中提取Token的三个部分:

header = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
payload = "eyJwIjoxLCJ0IjoxNTgwNzU2MDc3MDAwLCJiIjoiMzIwOTEzMCIsInciOjEwMDAsImsiOiJ7XCJ1XCI6XCJKMUR1VTdiTDJEbz1cIixcImlcIjpcIlhXeFM4STh1N0xuN0F6YThpVStqL2c9PVwiLFwidFwiOlwieFQ3c3V3UzB5ZytXRjdvZkVvcnFMdz09XCIsXCJiXCI6XCJ5aUw2S3ZSV2o4az1cIixcIm5cIjpcIm9PckxZR3BSZ3lvPVwifSIsImlhdCI6MTU4MDc1NjA3N30"
signature = "_z5Ji3WCV2urRLzEWWRDToqoGX9tl4Mtts9f7kxRtME"

将header和payload部分进行解码:

header_str = base64.b64decode(header).decode()
print(header_str)
payload_str = base64.b64decode(payload).decode()
print(payload_str)

{“alg”:“HS256”,“typ”:“JWT”}
binascii.Error: Incorrect padding

解码后发现header部分解码正常,而payload部分无法解码。这是因为过长文本在进行Base64编码时,如果长度为奇数则会使用等号占位拼接为偶数。这里尝试在payload的后面拼接一个等号:

payload = "eyJwIjoxLCJ0IjoxNc.....6MTU4MDc1NjA3N30="
payload_str = base64.b64decode(payload).decode()
print(payload_str)

{“p”:1,“t”:1580756077000,“b”:“3209130”,“w”:1000,“k”:"{“u”:“J1DuU7bL2Do=”,“i”:“XWxS8I8u7Ln7Aza8iU+j/g==”,“t”:“xT7suwS0yg+WF7ofEorqLw==”,“b”:“yiL6KvRWj8k=”,“n”:“oOrLYGpRgyo=”}",“iat”:1580756077}

成功解码payload。

解析明文参数

我们发现header是固定不变的,无需解析。

{alg: "HS256", typ: "JWT"}

将payload通过JSON格式展示:

{
	"p": 1,
	"t": 1580756077000,
	"b": "3209130",
	"w": 1000,
	"k": "{\"u\":\"J1DuU7bL2Do=\",\"i\":\"XWxS8I8u7Ln7Aza8iU+j/g==\",\"t\":\"xT7suwS0yg+WF7ofEorqLw==\",\"b\":\"yiL6KvRWj8k=\",\"n\":\"oOrLYGpRgyo=\"}",
	"iat": 1580756077
}

通过多次使用不同的Token解码配合调试JS进行比对,得出规律:

{
	"p": 当前页码,
	"t": 当前时间戳取整 * 1000,
	"b": "当前书籍ID",
	"w": 1000,  //固定为1000,猜测可能是Token时效性,单位:s
	"k": "未知规律的JSON数据",
	"iat": 当前时间戳取整
}

分析完成后,仅剩「k」参数未分析成功。

再次调试JavaScript,解析「k」参数

由于「k」参数未知,再次调试JavaScript尝试解析。

回到Chrome控制台,重新向getJwt的函数末尾「1892」下断点,并重新载入。
在这里插入图片描述

此处发现参数「k」是根据「this.$store.state.aesK」产生的:

k: this.$store.state.aesK

通过搜索「aesK」,可以发现「aesK」是由「setK」函数产生的。删除原断点并向函数末尾添加断点,重新载入网页。
在这里插入图片描述

此处并没有获得更多的信息,只知道「aesK」是由临时JSON变量「 t 」转化为字符串产生的。

再次向上搜索「setK」,下断点并刷新,看看这个临时变量「 t 」到底是怎么来的。

在这里插入图片描述

发现进入了滚动监听中,找到「setK」函数由「e.$store.commit」执行,临时变量「 t 」为参数 i[“data”][“data”] ,推测该函数为网络请求函数。

向控制台发送「 i 」命令,查看临时变量「 i 」的详细信息:

在这里插入图片描述

果然「 i 」为网络请求的响应对象,url,method,params,headers,一应俱全。

五、自由生成可用参数

构造自己的「k」参数

通过得到的「 i 」对象,分析HTTP请求:

  • 构造url:
    	base_url = 'https://lib-nuanxin.wqxuetang.com'
    	url = base_url + "/v1/read/k"
    
  • 分析params:
    	params = {"bid": "书籍ID"}
    

生成请求:

import requests

base_url = 'https://lib-nuanxin.wqxuetang.com'
url = base_url + "/v1/read/k"
params = {"bid": "3209130"}
response = requests.get(url, params=params)
print(response.text)

{“data”:{“u”:“hdo/TZw9R0E=”,“i”:“sDHlcVZ8PjrF2TYOVQHjKg==”,“t”:“usVhJQJ/S/DWIl6F1onOrQ==”,“b”:“Ma4GV4NJ+XQ=”,“n”:“Ip8NYew72NU=”},“errcode”:0,“errmsg”:“success”}

与Token中解析出来的「k」参数对比,从响应中提取出正确的「k」:

Token:

{\"u\":\"J1DuU7bL2Do=\",\"i\":\"XWxS8I8u7Ln7Aza8iU+j/g==\",\"t\":\"xT7suwS0yg+WF7ofEorqLw==\",\"b\":\"yiL6KvRWj8k=\",\"n\":\"oOrLYGpRgyo=\"}"

Response:

{"data":{"u":"hdo/TZw9R0E=","i":"sDHlcVZ8PjrF2TYOVQHjKg==","t":"usVhJQJ/S/DWIl6F1onOrQ==","b":"Ma4GV4NJ+XQ=","n":"Ip8NYew72NU="},"errcode":0,"errmsg":"success"}

可以看出,Token中的数据为dumps后的Response中的[“data”]字段,转义符经测试发现对生成结果没有影响。

接下来编写函数来获得自己的「k」参数:

import requests


def get_aesK(book_id):
    base_url = 'https://lib-nuanxin.wqxuetang.com'
    url = base_url + "/v1/read/k"
    params = {"bid": str(book_id)}
    response = requests.get(url, params=params)
    return json.dumps(response.json()['data'])


if __name__ == '__main__':
    k = get_aesK(3209130)
    print(k)

{“u”: “GhXWKjHdeEw=”, “i”: “e7noyNxb36ZZJibtwrCj3Q==”, “t”: “qCYOHAiU7Z3SERESsXlVlQ==”, “b”: “TCeKETGsy4Y=”, “n”: “+KviyUw5oAw=”}

构造自己的Token

编写函数来生成自己的header:

def init_header():
    return {"alg": "HS256", "typ": "JWT"}

编写函数来生成自己的payload:

def init_payload(book_id, page, k):
    return {
        "p": page,
        "t": time.time() * 1000,
        "b": str(book_id),
        "w": 1000,
        "k": k,
        "iat": time.time()
    }

现在header和payload都已经解析完成,还需要Jwt-HS265加密的最后一个参数:Signature

Signature部分由Base64编码后的header和payload拼接,中间用点分隔 ,之后再加入Secret通过HS256算法加密后得到。

调试 JavaScript 寻找「Secret」参数

想要得到「Signature」需要使用「Secret」进行加密,我们需要继续调试JavaScript来寻找「Secret」参数

在控制台中通过搜索直接得到「Secret」参数
在这里插入图片描述

本以为这个最关键的密钥会很难拿到,甚至不会出现在客户端。但由于Token是需要用JavaScript动态生成的,所以这个密钥以明文方式出现在这也情有可原。

六、重新加密参数,获得可用的图片链接

编写init_imgSrc和init_token函数,参数为「书籍ID」和「页码」:

import json
import time
import jwt

import requests


def get_aesK(book_id):
    base_url = 'https://lib-nuanxin.wqxuetang.com'
    url = base_url + "/v1/read/k"
    params = {"bid": str(book_id)}
    response = requests.get(url, params=params)
    return json.dumps(response.json()['data'])


def init_payload(book_id, page, k):
    return {
        "p": page,
        "t": time.time() * 1000,
        "b": str(book_id),
        "w": 1000,
        "k": k,
        "iat": time.time()
    }


def init_header():
    return {"alg": "HS256", "typ": "JWT"}


def init_token(book_id, page):
    k = get_aesK(book_id)

    header = init_header()
    payload = init_payload(book_id, page, k)
    secret = 'g0NnWdSE8qEjdMD8a1aq12qEYphwErKctvfd3IktWHWiOBpVsgkecur38aBRPn2w'
    return jwt.encode(headers=header, payload=payload, key=secret).decode()


def init_imgSrc(book_id, page):
    token = init_token(book_id, page)
    return 'https://lib-nuanxin.wqxuetang.com/page/img/{}/{}?k={}'.format(book_id, page, token)


if __name__ == '__main__':
    src = init_imgSrc(3209130, 1)
    print(src)

https://lib-nuanxin.wqxuetang.com/page/img/3209130/1?k=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwIjoxLCJ0IjoxNTgwNzc1MzUyNjUyLjgxNiwiYiI6IjMyMDkxMzAiLCJ3IjoxMDAwLCJrIjoie1widVwiOiBcIkdoWFdLakhkZUV3PVwiLCBcImlcIjogXCJlN25veU54YjM2WlpKaWJ0d3JDajNRPT1cIiwgXCJ0XCI6IFwiUjFkVUp6VkFXV1k5V2Q2TUhvZXJYQT09XCIsIFwiYlwiOiBcIlRDZUtFVEdzeTRZPVwiLCBcIm5cIjogXCIrS3ZpeVV3NW9Bdz1cIn0iLCJpYXQiOjE1ODA3NzUzNTIuNjUyODE3fQ.hpoDtSKzo7XBDuWIzNueRH0AlXjn7olAtpAJkebLEYs

Tips:由于Token可能由时效性,该链接可能会失效。

使用Postman成功下载该图片:
在这里插入图片描述

总结

文泉的解密主要在于了解Jwt-HS256加密的规则,调试JS的部分比较少。因为base64不具备安全性的机制,可以将直接参数解码为明文,同时Secret以明文方式展示给了客户端,也未在JS中设置偏移量等加密内容,避免了大量调试JS找参和调参的过程,总体难度中等偏上,适合想要进阶学习的朋友尝试。

编辑于 Tue Feb 4 08:43:14 2020
左素


  1. 解决Python修改pyjwt模块默认header无效的问题
    https://blog.csdn.net/m0_46261074/article/details/104165261 ↩︎

发布了2 篇原创文章 · 获赞 3 · 访问量 391

猜你喜欢

转载自blog.csdn.net/m0_46261074/article/details/104162067