第11章 day11-day12关于json请求体/逆向爬虫实战

第1知识点:关于json请求体
第2知识点:关于精准请求(如何排除干扰请求)
第3知识点:入口定位
一、关键字方法
(1) 方法关键字 encrypt decrypt
(2) key关键字

第4知识点:断点与断点调试
普通断点
XHR断点
条件断点
日志断点
脚本断点

点击查看01 福建省电子交易平台.py代码
import requests

# 发起一个HTTP协议包四要素:
# -- (1) url
# -- (2) 请求方式 get/post请求
# -- (3) 请求头
# -- (4) 请求载荷(get请求走查询参数或者post请求走请求体)

# http协议格式:
# 方式1
"""
get url?查询参数 http/1.1
请求头1
请求头2
...
请求头n

请求体         
"""
# 方式2
"""
post url http/1.1
请求头1
请求头2
...
contentType:form
请求头n

user=yuan&pwd=123 :form格式    
"""
# 方式3

"""
post url http/1.1
请求头1
请求头2
...
contentType:json
请求头n
 
{"user":"yuan","pwd":123}:json格式      
"""

my_headers = {
    'Accept': 'application/json, text/plain, */*',
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Content-Type': 'application/json;charset=UTF-8',
    'Origin': 'https://ggzyfw.fujian.gov.cn',
    'Pragma': 'no-cache',
    'Referer': 'https://ggzyfw.fujian.gov.cn/business/list/',
    'Sec-Fetch-Dest': 'empty',
    'Sec-Fetch-Mode': 'cors',
    'Sec-Fetch-Site': 'same-origin',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
    'portal-sign': '02f433c6b30db0154f54687b5438d7f1',
    'sec-ch-ua': '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"macOS"',
}
data = {
    'pageNo': 2,
    'pageSize': 20,
    'total': 2797,
    'AREACODE': '',
    'M_PROJECT_TYPE': '',
    'KIND': 'GCJS',
    'GGTYPE': '1',
    'PROTYPE': '',
    'timeType': '6',
    'BeginTime': '2024-09-29 00:00:00',
    'EndTime': '2025-03-29 23:59:59',
    'createTime': '',
    'ts': 1743234193847,
}
url = "https://ggzyfw.fujian.gov.cn/FwPortalApi/Trade/TradeInfo"
# data:默认数据处理成form表单格式
# res = requests.post(url, headers=my_headers, data=data)
# data:数据处理成json格式
res = requests.post(url, headers=my_headers, json=data)
print(res.text)
点击查看02 福建省电子交易平台2.py代码
import base64
import json

import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

headers = {
    'Accept': 'application/json, text/plain, */*',
    'Accept-Language': 'zh-CN,zh;q=0.9',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Content-Type': 'application/json;charset=UTF-8',
    'Origin': 'https://ggzyfw.fujian.gov.cn',
    'Pragma': 'no-cache',
    'Referer': 'https://ggzyfw.fujian.gov.cn/business/list/',
    'Sec-Fetch-Dest': 'empty',
    'Sec-Fetch-Mode': 'cors',
    'Sec-Fetch-Site': 'same-origin',
    'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
    'portal-sign': 'a56891e739cdb7563faf1740f9aa77b4',
    'sec-ch-ua': '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
    'sec-ch-ua-mobile': '?0',
    'sec-ch-ua-platform': '"macOS"',
}

json_data = {
    'pageNo': 3,
    'pageSize': 20,
    'total': 2798,
    'AREACODE': '',
    'M_PROJECT_TYPE': '',
    'KIND': 'GCJS',
    'GGTYPE': '1',
    'PROTYPE': '',
    'timeType': '6',
    'BeginTime': '2024-09-29 00:00:00',
    'EndTime': '2025-03-29 23:59:59',
    'createTime': '',
    'ts': 1743239013232,
}

response = requests.post('https://ggzyfw.fujian.gov.cn/FwPortalApi/Trade/TradeInfo', headers=headers, json=json_data)

# print(response.text)

# 基于Python做出AES的解密
# (1) base64解码
base64_encrypt_data = response.json().get("Data")
# print(base64_encrypt_data)

encrypt_data = base64.b64decode(base64_encrypt_data)
# print(encrypt_data)

# (2) aes解密
k = 'EB444973714E4A40876CE66BE45D5930'.encode()
i = 'B5A8904209931867'.encode()
aes = AES.new(key=k, mode=AES.MODE_CBC, iv=i)
data = aes.decrypt(encrypt_data)
data = unpad(data, 16)
data = json.loads(data)
print(data)

for i in data["Table"]:
     print(i.get("NAME"))

点击查看03 福建省电子交易平台3.py代码
import time
import base64
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from hashlib import md5
import json

headers = {

}


# 生成sign值
def get_sign(data):
    # (1) 剔除空值
    new_data = {}
    for key, val in data.items():
        # print(key, val)
        if val == "" or val == 0:
            continue
        new_data[key] = val

    # print(new_data)

    # (2) 排序
    s = new_data.items()
    # print(s)
    ret = sorted(s, key=lambda item: item[0])
    # print(ret)
    s2 = ""
    for key, val in ret:
        s2 += key + str(val)

    # print(s2)
    ra = "B3978D054A72A7002063637CCDF6B2E5"

    n = ra + s2

    # (3) 生成md5值
    m = md5()
    m.update(n.encode())
    sign = m.hexdigest()
    # print(sign)  # 4145a19057c83fd6a99163d0bd2f5e88

    return sign


def decrypt(res):
    # 基于Python做出AES的解密
    # (1) base64解码
    base64_encrypt_data = res.json().get("Data")
    # print(base64_encrypt_data)

    encrypt_data = base64.b64decode(base64_encrypt_data)
    # print(encrypt_data)

    # (2) aes解密
    k = 'EB444973714E4A40876CE66BE45D5930'.encode()
    i = 'B5A8904209931867'.encode()
    aes = AES.new(key=k, mode=AES.MODE_CBC, iv=i)
    data = aes.decrypt(encrypt_data)
    data = unpad(data, 16)
    data = json.loads(data)
    print(data)

    for i in data["Table"]:
        print(i.get("NAME"))


def main():
    for i in range(1,10):
        json_data = {
            "ts": int(time.time() * 1000),
            "pageNo": i,
            "pageSize": 20,
            "total": 2798,
            "AREACODE": "",
            "M_PROJECT_TYPE": "",
            "KIND": "GCJS",
            "GGTYPE": "1",
            "PROTYPE": "",
            "timeType": "6",
            "BeginTime": "2024-09-29 00:00:00",
            "EndTime": "2025-03-29 23:59:59",
            "createTime": ""
        }
        sign = get_sign(json_data)

        headers["portal-sign"] = sign

        response = requests.post('https://ggzyfw.fujian.gov.cn/FwPortalApi/Trade/TradeInfo', headers=headers,
                                 json=json_data)
        # 解密
        decrypt(response)
        time.sleep(1)


if __name__ == '__main__':
    main()

逆向爬虫实战

JS逆向爬虫是一种通过分析和执行JavaScript代码来提取动态网页数据的技术。随着现代网站越来越多地使用JavaScript进行内容加载,传统的爬虫方法(如仅使用HTTP请求)常常无法获取所需的数据,因此需要使用逆向工程技术来解析JavaScript。

1. Python逆向解密

某目标资源交易平台首页,PageList即目标接口

image-20240703下午52816398

先通过Python基础爬虫抓取该响应数据,这里可以复制curl信息去转换为爬虫代码,这里给大家推家一个不错的网站:

https://curlconverter.com/
image-20240703下午54143007

image-20240703下午54410686

image-20240703下午53953108

但是很明显,服务器对该URL线路返回的数据做了某种加密,客户端浏览器该URL请求拿到的也是加密数据,但是,浏览器的该网站窗口在发起第一次首页请求的时候获取了大量的JS逻辑代码,即客户端和该网站交流的正确姿势,所以目标请求(PageList)返回的加密数据,会有后续的某段JS代码进行解密,那么怎么从千千万万行JS代码中找到解密的位置呢,我们在JS逆向中主要有几下几种技巧

  • 关键字搜索
  • 请求堆栈
  • hook钩子

我们先来学习第一个,关于关键字搜索,即客户端浏览器接收到的该网站所有文件(主要是JS文件)中全局搜索关键字,那么搜索关键字,常用的有以下几种

  • 方法关键字(比如encrypt,decrypt,JSON.stringify,JSON.parse等)
  • key关键字
  • 路径关键字
  • header关键字
  • 拦截方法关键字

我们一般首选的是方法关键字,这背后的逻辑就是我们怀疑JS加密代码也会用到encrypt这个名字,解密会用到decrypt,那有没有可能人家写的JS并有没有用呢,当然,所以这只是思路的一种,我们爬虫逆向就是有逻辑的连蒙带猜,没有一招屡试不爽,只能按经验不断测。因为我们要找解密的算法,所以搜索一下decrypt那么怎么全局搜索呢?如图所示

image-20240703下午61501765

搜到了太多,不好排查,所以我们要进一步加限制,那么接下来的思路就是如果JS解密用到decrypt,应该大概率(注意没有绝对一说)是作为一个函数调用,所以我们尝试搜索decrypt(,这样就只有8个嫌疑点,方便后续排查了

image-20240703下午61848312

加下来就是最精彩的地方了,这八个嫌疑犯到底谁是解密的元凶呢?我们可以通过断点技术进行排查,即给这八个位置都加上断点,然后重新触发PageList请求的事件,那么请求发出,数据返回,本地JS解密,如果元凶在这八个嫌疑犯中,那么一定会被执行,又因为加了断点,即一定会被断下来,所以结论是,能被断下来的99%就是解密位置,所以先加断点:

image-20240703下午63145847

我们可以发现,这里明显用得是AES算法,通过鼠标悬浮显示或者控制台打印,可以判断出t就是解密的数据,r["e"]就是key,r["i"]就是iv

image-20240703下午63928775

至此,知道了是aes算法,又找到了key和iv,我们就可以回到开始的代码中完成解密实现了!

image-20240703下午65519914

2. Python逆向加密

爬虫的核心是海量抓取数据,虽然现在破解了解密逻辑,但是我们可以批量爬取数据吗?

image-20240703下午112538264

事实是我们改变data中的任何一个参数,比如我们想修改pageSize值,即获取其它页数的数据时,就直接被服务器告知咱们是恶意请求了,它是怎么知道这是一个爬虫程序的呢?原因很简单,浏览器执行本地的本地JS发出这次请求前以这些数据为参数生成了一个加密值,即请求头中的portal-sign,也就是json_dataportal-sign是对应的,关联的。json_dataportal-sign一起发到服务器,服务器会解密portal-sign再和json_data比对,如果不一致,说明不是正常请求,有数据篡改,所以响应我们一个警告。

image-20240703下午113303674

有同学可能会问,那我们不要改数据和sign值不就一致了嘛,但是问题在于,不修改查询你怎么批量爬虫呢,我想要每一页的数据,理论上我们更改pageSize就好了,但是对方服务器做出了反爬,所以我们必须根据不同的page等查询参数计算出与之配对的sign才能通过服务器的校验。所以必须破解客户端JS代码根据参数如何生成的sign值。

上面的解密是ajax请求回来,本地的JS做的解密,这一步我们要破解的是发送ajax请求,携带的参数portal-sign的加密函数,所以思路也可以想逆向解密,方法关键字,搜关键字encrypt,同样因为JS做加密时大概率调用算法库,函数名很大概率是叫encrypt,于是全局搜索

image-20240924下午44450480

给这13个位置都加上断点,然后刷新页面,触发目标请求,发现没有任何一个端点断下来,说明这个方法关键字失效了,这很正常,没有什么技巧一招鲜吃遍天。接下来我们换key关键字。

key关键字的核心是这个sign值在本地生成,直接搜是搜不到的,但是我们想一下,这个值生成后紧接着的操作应该是什么呢?是不是应该大概率要和键portal-sign组成键值对

image-20240924下午44734045

所以我们全局搜索portal-sign,右边如何是个函数调用再赋值就很可能是我们的加密函数,格式可如下:

portal-sign = xxx()
或者
{
portal-sign: xxx()
}

全局搜索:

image-20240924下午45613649

搜索出一个位置,加上断点确认,刷新页面,果然被断住,参数检查也的确是查询参数,所以这个f.getSign就是我们要找的关于portal-sign的生成位置。

image-20240924下午50308270

image-20240924下午50347557

这个d函数内代码就是sign生成的逻辑。

image-20240924下午50845279

断点d函数第一行,进入当前断点,解析d的算法逻辑。

  • 判断参数对象的各个键的值有没有空值(一般没有空值,所以这一步对数据实际没什么操作)
  • r["a"]是一个固定字符串,每次请求都一样
  • u(t)是将参数对象t排序后组装成指定格式的字符串,比如将对象{a:1,b:2}组装成"a1b2"
  • 将r["a"]的固定字符串和u(t)生成的参数字符串拼接在一起,得到n

最关键的是最后一步s(n)做了什么,接下来在s(n)的位置加上断点,进入到s(n)中

image-20240924下午51516363

发现s函数就是md5的摘要算法,即最后一步就是将n字符串计算md5值作为portal-sign。

至此,关于portal-sign的加密逻辑就完全破解了。

所以我们接下来只需要在python的爬虫代码中像JS那样根据具体的参数生成固定的portal-sign即可请求通过。

代码如下:

from hashlib import md5
import time
import requests
import base64
import json
from Crypto.Util.Padding import pad, unpad

from Crypto.Cipher import AES

timer = int(time.time() * 1000)

cookies = {
   # 略
}

headers = {
   # 略
}

def decrypt(response):
    base64_encrypt_data = response.json().get("Data")

    # 一、 base64的解码

    encrypt_data = base64.b64decode(base64_encrypt_data)
    print("encrypt_data:", encrypt_data)

    # 二、解密数据
    # (1) 确认key和iv必须保证是16或者24,或者32
    key = 'EB444973714E4A40876CE66BE45D5930'.encode()
    iv = 'B5A8904209931867'.encode()
    # (2) 构建一个aes对象
    aes = AES.new(key, AES.MODE_CBC, iv)

    # (3) 对数据解密
    data = aes.decrypt(encrypt_data)
    data = unpad(data, 16).decode()
    data = json.loads(data).get("Table")
    print(data)


# 根据data生成sign值

def get_sign(data):
    # (1) 固定字符串
    s = 'B3978D054A72A7002063637CCDF6B2E5'
    # (2) 将参数整理成某称格式字符串
    l = sorted(data.items(), key=lambda i: i[0])
    data_str = ""
    for key, val in l:
        data_str += key + str(val)

    print("data_str:::", data_str)

    # (3)
    s = s + data_str
    md5_obj = md5()
    md5_obj.update(s.encode())

    return md5_obj.hexdigest()


def main():
    data = {
        'type': '12',
        'IS_IMPORT': 1,
        'pageSize': 6,
        'ts': timer,
    }

    sign = get_sign(data)
    print("sign:::", sign)
    headers["portal-sign"] = sign
    url = 'https://xxxxx.fj.gov.cn/FwPortalApi/Article/PageList'
    response = requests.post(url, cookies=cookies, headers=headers,
                             json=data)

    decrypt(response)



if __name__ == '__main__':
    main()

这样我们就完成了整个加密逆向和解密逆向的全部破解,实现了批量明文数据抓取的目的。

3. JS逆向加密与解密

上面我们找到加密函数get_sign后,进入内部,找到了d函数,通过读取d函数的加密逻辑,再用Python代码去实现,这种方式叫Python逆向,但是这里有一个问题,就是d函数如果非常复杂呢,有一百个步骤,我们一是读取逻辑困难,二是通过Python复现麻烦,所以这种方式并不理想,我们真正的常见的玩法是JS逆向,这才是”主角“终于登场。

所谓JS逆向的概念很简单,放在这个案例中,就是当我们找到d函数,不要再去“收拾”里面的逻辑,而是把整个d函数“铲走”到本地,然后用一个叫node.js的解释器来运行出结果portal-sign,为爬虫代码所调用。

我们在本地先创建一个名为main.js的文件

function d(t) {
    for (var e in t)
        "" !== t[e] && void 0 !== t[e] || delete t[e];
    var n = r["a"] + u(t);
    return s(n).toLocaleLowerCase()
}

// 测试d函数
// t就是模拟的查询参数对象
t = {
    "ts": 1727168121817,
    "type": "12",
    "IS_IMPORT": 1,
    "pageSize": 3
}
sign = d(t)
console.log(sign)

可以右击run执行该js文件,也可以在终端通过node main,js来运行,结果报错:

image-20240924下午61649383

找不到r变量,因为我们分析知道r["a"]是固定值,所以直接替换就可以了,然后继续报错:

image-20240924下午61825522

这个很好理解,我们只拷贝了d函数,而d函数虽然是生成sign的主函数,但依赖了外部的其他函数或者变量,所以我们需要将报错的依赖也拷贝过来,保证d函数的顺利执行

image-20240924下午62150146

u函数中又用到l,所以一起拷贝到本地,如下:

function l(t, e) {
    return t.toString().toUpperCase() > e.toString().toUpperCase() ? 1 : t.toString().toUpperCase() == e.toString().toUpperCase() ? 0 : -1
}

function u(t) {
    for (var e = Object.keys(t).sort(l), n = "", a = 0; a < e.length; a++)
        if (void 0 !== t[e[a]])
            if (t[e[a]] && t[e[a]] instanceof Object || t[e[a]] instanceof Array) {
                var i = JSON.stringify(t[e[a]]);
                n += e[a] + i
            } else
                n += e[a] + t[e[a]];
    return n
}


function d(t) {
    for (var e in t)
        "" !== t[e] && void 0 !== t[e] || delete t[e];
    var n = 'B3978D054A72A7002063637CCDF6B2E5' + u(t);
    return s(n).toLocaleLowerCase()
}

// 测试d函数
// t就是模拟的查询参数对象
t = {
    "ts": 1727168121817,
    "type": "12",
    "IS_IMPORT": 1,
    "pageSize": 3
}
sign = d(t)
console.log(sign)

最后报错:

image-20240924下午62312488

最后我们通过进入s函数中知道是md5算法,涉及到算法,就到了最后一步,不用搬移算法函数,因为我们可以在本地调用js的算法库去“平替”该代码:

const CryptoJS = require('crypto-js');

function l(t, e) {
    return t.toString().toUpperCase() > e.toString().toUpperCase() ? 1 : t.toString().toUpperCase() == e.toString().toUpperCase() ? 0 : -1
}

function u(t) {
    for (var e = Object.keys(t).sort(l), n = "", a = 0; a < e.length; a++)
        if (void 0 !== t[e[a]])
            if (t[e[a]] && t[e[a]] instanceof Object || t[e[a]] instanceof Array) {
                var i = JSON.stringify(t[e[a]]);
                n += e[a] + i
            } else
                n += e[a] + t[e[a]];
    return n
}


function d(t) {
    for (var e in t)
        "" !== t[e] && void 0 !== t[e] || delete t[e];
    var n = 'B3978D054A72A7002063637CCDF6B2E5' + u(t);
    return CryptoJS.MD5(n).toString().toLocaleLowerCase()
}

// 测试d函数
// t就是模拟的查询参数对象
t = {
    "ts": 1727168121817,
    "type": "12",
    "IS_IMPORT": 1,
    "pageSize": 3
}
sign = d(t)
console.log(sign)

结果生成:

image-20240924下午62655029

接下来就是Python怎么调用这个JS代码了,这里用到的是一个Python的模块:

image-20240924下午64525132

解密其实也是一样的,定位到解密的b函数,直接拷贝走

image-20240924下午72347609

image-20240924下午72731622

key和iv是固定的,直接平替,h.a是算法库对象,也用本地npm的替换,结果如下:

function b(t) {
    var e = CryptoJS.enc.Utf8.parse('EB444973714E4A40876CE66BE45D5930')
        , n = CryptoJS.enc.Utf8.parse('B5A8904209931867')
        , a = CryptoJS.AES.decrypt(t, e, {
        iv: n,
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    });
    return a.toString(CryptoJS.enc.Utf8)
}

最后在Python中爬到加密数据通过调用b函数实现解密,整个过程就是完整的JS逆向。

posted @ 2025-09-29 16:53  凫弥  阅读(44)  评论(0)    收藏  举报