【爬虫实战】使用Python和JS逆向获取易车网汽车参数详情

前言

有的网站请求参数或者URL是明文的,但是有些是加密后的。接下来以易车网为例,巩固一下逆向思路。

一、目标分析

汽车参数

使用接口获取汽车的参数配置,但是请求参数无法直接阅读,点击解码即可。

请求头里还有一下加密参数

估计重点就是X-Sign,其他的可能用处不大。

其实可以试试直接请求接口,看是否有返回值:

1
2
3
4
5
6
// https://mapi.yiche.com/web_api/car_model_api/api/v1/car/config_new_param?cid=508&param=%7B%22cityId%22%3A%22201%22%2C%22carId%22%3A%22166210%22%7D

{
"message": "公共参数缺失",
"status": "11036"
}

所以重点来了,可能就是这个X-Sign起决定性因素。

二、逻辑分析

1. 定位函数获取X-Sign

只有两条记录,全部打上断点,进去哪个哪个就是。

看输出的值,有点像。

继续分析这句代码:

1
"headers" == e.encryptType && (n["x-sign"] = u(e, t));

执行规则:

  1. 只要“&&”前面是false,无论“&&”后面是true还是false,结果都将返“&&”前面的值;
  2. 只要“&&”前面是true,无论“&&”后面是true还是false,结果都将返“&&”后面的值;

总结:假前真后

所以有效的的值一定是u(e, t)

接下来分别看一下e和t这两个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// t
{
"cid": "508",
"ver": "v10.80.0",
"timestamp": 1700192890484,
"gradeParam": {},
"uid": "",
"headerEncryptKeys": [
{
"name": "pc",
"value": "19DDD1FBDFF065D3A4DA777D2D7A81EC",
"cid": "508"
},
{
"name": "phone",
"value": "DB2560A6EBC65F37A0484295CD4EDD25",
"cid": "601"
},
{
"name": "h5",
"value": "745DFB2027E8418384A1F2EF1B54C9F5",
"cid": "601"
},
{
"name": "business_applet",
"value": "64A1071F6C3C3CC68DABBF5A90669C0A",
"cid": "601"
},
{
"name": "wechat",
"value": "AF23B0A6EBC65F37A0484395CE4EDD2K",
"cid": "601"
},
{
"name": "tencent",
"value": "1615A9BDB0374D16AE9EBB3BBEE5353C",
"cid": "750"
}
],
"paramsKey": "f48aa2d0-31e0-42a6-a7a0-64ba148262f0"
}

然后是e:

这不太对啊,接口地址不是这个,那就继续执行,当URL是config_new_param再追踪。

果然有执行了几次才拿到了我们想要的结果。

也就是说如果开始找出了URL,那么后面的一切都是南辕北辙。

接下来就跳转到了s函数:

至此,我们找到了X-Sign的位置,之后使用JS来补环境就可以了吗?不要着急,执行一下没准能得到意想不到的结果。

好像恰巧获取的cid和param的值,而X-Sign就是加密后的密文。

2. 使用代码实现X-Sign

刚才我们获取了cid=508&param={"cityId":"201","carId":"166210"}19DDD1FBDFF065D3A4DA777D2D7A81EC1700201316661这个值,把这个值传给h函数,并把相同作用域的函数全部复制过来就可以了。很顺利,一次就跑通了。

接下来思考一下如何获取这个参数变量。我们回到刚才的s函数,可以看到这个参数是由n提供的。

可以看到就是把n参数进行一个MD5的处理,这时候我们可以猜测一下是不是普通的MD5,可以用python或者js代码试试:

1
2
3
4
5
import hashlib
content = 'cid=508&param={"cityId":"201","carId":"166210"}19DDD1FBDFF065D3A4DA777D2D7A81EC1700211170720'
md5_hash = hashlib.md5(content.encode()).hexdigest()
print(md5_hash)
# 输出 166cc7dd317a737794ecaa2bd795f37d

可以看到和结果中是一致的。这就简单很多了,大部分逻辑就分析完了。

3. 编写Python代码,进行调用

根据前面的分析,可以确定以下参数是需要传值的:

  • 查询参数 {"cityId":"201","carId":"166210"}
  • 时间戳,可以从Python传过去,也可以直接使用JS来生成

其他值可以是固定或者处理过的。

三、代码实现

全部代码如下,使用python将字符串进行MD5处理,然后发出请求。需要注意以下几个问题:

  1. 有些字段是必须存在的,比如X开头几个,因为前面的JS代码中可以看到有使用到,否则就会报错;
  2. python的JSON数据和JS的JSON是有区别的,需要单独处理一下,否则校验失败。

以下代码是请求一种车型的方式,如果是批量请求,提前准备好多个ID就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import json
import time
import hashlib
import requests


def start(city_data):
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/119.0",
"Content-Type": "application/json;charset=UTF-8",
"X-Platform": "pc",
}
ts = str(int(time.time() * 1000))
new_data = json.dumps(city_data, separators=(',', ':'))
s1 = f'cid=508&param={new_data}19DDD1FBDFF065D3A4DA777D2D7A81EC' + ts
print(s1)
md5_hash = hashlib.md5(s1.encode()).hexdigest()
print(md5_hash)
headers['Referer'] = 'https://car.yiche.com/biyadifsuv/m166210/peizhi/'
headers['X-Sign'] = md5_hash
headers['X-Timestamp'] = ts
headers['X-City-Id'] = "201"
headers['Cid'] = "508"
params = {
"cid": "508",
"param": json.dumps(city_data, separators=(',', ':'))
}
print(params)
# 注意要使用separators,否则会出现签名验证失败的问题,使用后间隔更小,符合网站中JS的要求
# 'cid=508&param={"cityId":"201","carId":"166210"}19DDD1FBDFF065D3A4DA777D2D7A81EC1700211170720',
# 否则会出现后面的两种情况
# {'cid': '508', 'param': '{"cityId": "201", "carId": "166210"}'}
# {'cid': '508', 'param': '{"cityId":"201","carId":"166210"}'}

res = requests.get(
'https://mapi.yiche.com/web_api/car_model_api/api/v1/car/config_new_param', params=params,
headers=headers,
)
print(res.json()['message'])


if __name__ == '__main__':
city_data = {
"cityId": "201", "carId": "166210"
}
start(city_data)

返回结果:

四、总结回顾

今天分析JS逻辑的时候在md5这一块卡了很久,主要原因如下;

  1. 起初看到进行MD5处理的时候怀疑了一下是不是基础的MD5,有没有进行特殊处理。为了图省事,直接问的GPT。结果这家伙输出了和控制台不一致的值,所以就当成JS特殊处理了。因此就把相关的函数单独拉出来进行处理,生成Sign值后传给python。但最后一直校验失败,百思不得其解。后来又多次反复查看使用Python生成MD5值后才发现了GPT的问题。但还是很奇怪为啥JS生成的MD5值不对呢,难到少复制什么函数或者变量了?
  2. 拼接参数的时候出现了Python和JS两个版本JSON不一致的问题,就是空格。
  3. 还发现个问题,在不同的浏览器上请求头存在大小写不一致的问题,但是并不影响结果。后来问了一下GPT,回答放在后面。

在HTTP协议中,请求头是不区分大小写的。这意味着,根据HTTP规范,请求头的字段名不应该被视为大小写敏感的。例如,Content-Typecontent-type在HTTP的角度来看是相同的。

然而,实际上在某些情况下,服务器和浏览器的处理可能会导致大小写的差异。一些服务器可能会将请求头字段名视为大小写敏感,而另一些则可能不敏感。

此外,浏览器和其他HTTP客户端有时会以不同的方式处理请求头。有些浏览器可能会按照规范的方式处理,而另一些可能会将请求头字段名规范化为特定的形式。

为了确保在不同浏览器和服务器之间具有一致的行为,最好遵循HTTP规范,将请求头字段名视为不区分大小写。在编写代码时,最好使用规范的大小写形式。

因此,还是需要经验,多动手多动脑。