反爬(二)

网站二处罚信息爬取破解

1、请求url,查看数据情况

首先根据网络请求的顺序,请求列表页看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
url = 'http://credit.shaanxi.gov.cn/queryPermitPublishPage.jspx'
post_data = {"lb": "xzcf", "pageNo": "1", "pageSize": "10"}
_Request_Header = {
"User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0',
}
try:
req = requests.post(url, data=post_data, headers=_Request_Header)
except Exception as e:
print(e)
else:
if req.status_code == 200:
print(req.text)

<<< {"total":-1,"pageCount":1,"pageNo":1,"columns":{"zh":["决定文号","处罚类别","相对人名称","处罚机关","处罚决定日期"],"en":["xy020102","xy020103","xy010101","xy020101","xy020107"]},"pageSize":10,"list":"a75yXqmRStbuWNG6esIpxGRyYd2ZshrzvW9wFLKTwPNBP74nGyY13VrrI8c6mGe2c04/xNVS+GmL\nLB4QkA61163PC8E31UQcmpVzMtd8eyoSDffABXjNPnTwiBTDYjEB6/HrNytCSiIezl/qIUkCEFyP\nwC1+t9Qu5ozKZ3jkkQ3G*********************************************************************************\nbNnRldjsqiI="}
以上***省略了若干内容

请求列表页后,返回的的关键信息被加密了,乍一看上去像base64编码,使用base64解码后发现没有用,于是猜想肯定该网站使用了js加密,需要找出来该加密函数(或者说解密函数)。

2、寻找js加密函数

找加密函数一般有两种方式

(1)在页面元素中加断点,观察它是如何一步步从原始的加密的字符串渲染成我们在浏览器中看到的样子的;

(2)观察该请求的调用栈,查看它调用的所有的方法,一般从最近一次调用的开始找(Chrome浏览器中Initiator这一列最上面的请求是最近调用的)。这里注意,看见诸如jquery之类的方法可以跳过。

这里我们使用第(2)种方式,查找到第一个除jquery之外的第一个请求点进去,发现一个关键的fun()函数,在fun函数的位置加断点(直接使用鼠标点击前面的行号即可),然后刷新页面,让浏览器执行到当前环境,然后在下面的console种输入fun按下回车,点击去查看该函数,观察到一条关键的语句:

var html = renderTable(BmuY1.to(page.list), page.columns.en);

在该语句处加断点,然后刷新页面,鼠标放到page.list上发现,其值正是我们苦苦寻找的加密过的字符串;由此我们推断,BmuY1.to()就是解密函数,为了证实我们的推断,可以在下面的console中输入BmuY1.to(page.list),回车后的输出确实是我们在浏览器中看到的内容,所以在console中输入BmuY1.to+回车便可定位到该函数,我们把这个函数抠出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var BmuY1 = {
to: function(str) {
var WcDdE2 = str["\x72\x65\x70\x6c\x61\x63\x65"](/[\r\n]/g, "");
var QGPyfek3 = CryptoJS["\x65\x6e\x63"]["\x55\x74\x66\x38"]["\x70\x61\x72\x73\x65"](window["\x63\x6f\x6e\x66\x69\x67"]["\x79"]);
var Fh4 = CryptoJS["\x44\x45\x53"]["\x64\x65\x63\x72\x79\x70\x74"]({
ciphertext: CryptoJS["\x65\x6e\x63"]["\x42\x61\x73\x65\x36\x34"]["\x70\x61\x72\x73\x65"](WcDdE2)
}, QGPyfek3, {
mode: CryptoJS["\x6d\x6f\x64\x65"]["\x45\x43\x42"],
padding: CryptoJS["\x70\x61\x64"]["\x50\x6b\x63\x73\x37"]
});
var D5 = Fh4["\x74\x6f\x53\x74\x72\x69\x6e\x67"](CryptoJS["\x65\x6e\x63"]["\x55\x74\x66\x38"]);
return JSON["\x70\x61\x72\x73\x65"](D5)
},
go: function(HUPR6) {
var CEjyIKk7 = HUPR6["\x72\x65\x70\x6c\x61\x63\x65"](/[\r\n]/g, "");
var kwc8 = CryptoJS["\x65\x6e\x63"]["\x55\x74\x66\x38"]["\x70\x61\x72\x73\x65"](window["\x63\x6f\x6e\x66\x69\x67"]["\x79"]);
var O9 = CryptoJS["\x44\x45\x53"]["\x65\x6e\x63\x72\x79\x70\x74"](CryptoJS["\x65\x6e\x63"]["\x55\x74\x66\x38"]["\x70\x61\x72\x73\x65"](CEjyIKk7), kwc8, {
mode: CryptoJS["\x6d\x6f\x64\x65"]["\x45\x43\x42"],
padding: CryptoJS["\x70\x61\x64"]["\x50\x6b\x63\x73\x37"]
});
return CryptoJS["\x65\x6e\x63"]["\x42\x61\x73\x65\x36\x34"]["\x73\x74\x72\x69\x6e\x67\x69\x66\x79"](O9["\x63\x69\x70\x68\x65\x72\x74\x65\x78\x74"])
}
};

发现里面有一个to函数,一个go函数,既然to函数是解密的,那go函数应该是加密的。

3、将其js函数修改为可执行的函数,并布置为js服务

我们拿到这个函数之后,接下来肯定是要想办法执行它,一种方式是使用python的可以执行js的库,另一种方式是将其布置为js服务,直接使用python来请求。无论使用哪些方式,我们都得先对抠出来的源代码稍作处理成为可以正常执行的函数。

这里面有很多诸如"\x72\x65\x70\x6c\x61\x63\x65"之类的字符,其实将其复制到浏览器的console中回车发现其为replace,其实这里不必改动,需要改动的地方为window["\x63\x6f\x6e\x66\x69\x67"]["\x79"]

因为这里window对象涉及到当前浏览器环境,我们直接执行代码的时候该值肯定与浏览器中不一样,我们把这段代码复制到console中,发现其值为"%Cf9!AMA@7mGXfz6",通过验证发现其为定值,所以我们将代码中的window["\x63\x6f\x6e\x66\x69\x67"]["\x79"]替换为"%Cf9!AMA@7mGXfz6"

其次,我们看到CryptoJS,这个第一印象是node自带的crypto库,但其实不是的,在”https://www.npmjs.com/package/package"上搜索`CryptoJS`,发现了`crypto-js`库,于是进行安装并导入。

再将其作为js服务的形式,最终代码为:

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
const CryptoJS = require('crypto-js')

function BmuY1_to(str) {
var WcDdE2 = str["\x72\x65\x70\x6c\x61\x63\x65"](/[\r\n]/g, "");
var QGPyfek3 = CryptoJS["\x65\x6e\x63"]["\x55\x74\x66\x38"]["\x70\x61\x72\x73\x65"]('%Cf9!AMA@7mGXfz6');
var Fh4 = CryptoJS["\x44\x45\x53"]["\x64\x65\x63\x72\x79\x70\x74"]({
ciphertext: CryptoJS["\x65\x6e\x63"]["\x42\x61\x73\x65\x36\x34"]["\x70\x61\x72\x73\x65"](WcDdE2)
}, QGPyfek3, {
mode: CryptoJS["\x6d\x6f\x64\x65"]["\x45\x43\x42"],
padding: CryptoJS["\x70\x61\x64"]["\x50\x6b\x63\x73\x37"]
});
var D5 = Fh4["\x74\x6f\x53\x74\x72\x69\x6e\x67"](CryptoJS["\x65\x6e\x63"]["\x55\x74\x66\x38"]);
return JSON["\x70\x61\x72\x73\x65"](D5)
}

function BmuY1_go(HUPR6){
var CEjyIKk7=HUPR6["\x72\x65\x70\x6c\x61\x63\x65"](/[\r\n]/g,"");
var kwc8=CryptoJS["\x65\x6e\x63"]["\x55\x74\x66\x38"]["\x70\x61\x72\x73\x65"]('%Cf9!AMA@7mGXfz6');
var O9=CryptoJS["\x44\x45\x53"]["\x65\x6e\x63\x72\x79\x70\x74"](CryptoJS["\x65\x6e\x63"]["\x55\x74\x66\x38"]["\x70\x61\x72\x73\x65"](CEjyIKk7),kwc8,{mode:CryptoJS["\x6d\x6f\x64\x65"]["\x45\x43\x42"],padding:CryptoJS["\x70\x61\x64"]["\x50\x6b\x63\x73\x37"]});
return CryptoJS["\x65\x6e\x63"]["\x42\x61\x73\x65\x36\x34"]["\x73\x74\x72\x69\x6e\x67\x69\x66\x79"](O9["\x63\x69\x70\x68\x65\x72\x74\x65\x78\x74"])
}

module.exports = function xinyongshanxiHandler (req, res) {
try {
let {
content,
encrypt,
} = req.payload
if (encrypt==='go'){
return BmuY1_go(content)
}
else{
return BmuY1_to(content)
}
} catch (err) {
console.error(err.message)
return ''
}
}

下面我们调用这个服务,来解析获取的列表页内容:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import requests
import json


class MySpider(object):
_Request_Header = {
"User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:61.0) Gecko/20100101 Firefox/61.0',
}

def __init__(self):
self.post_url = 'http://credit.shaanxi.gov.cn/queryPermitPublishPage.jspx'
self.post_data = {"lb": "xzcf", "pageNo": "1", "pageSize": "10"}
self._decrypt_url = 'http://192.168.20.141:3001/xinyongshanxi'
self.detail_url = 'http://credit.shaanxi.gov.cn/queryItemData.jspx'

def encrypt_or_decrypt(self, content, is_encrypt=False):
"""
# 加密解密函数
:param content: 要加密or解密的内容
:param is_encrypt: True表示加密,False表示解密;默认解密
:return: 返回加密or解密后的字符串
"""
try_times = 0
while try_times < 3:
try_times += 1
try:
response = requests.post(
url=self._decrypt_url,
data={"content": json.dumps(content) if is_encrypt else content,
"encrypt": "go" if is_encrypt else "to"},
)
except Exception as _e:
print(_e)
else:
if response.status_code == 200:
return response.text

def fetch_list(self):
try:
req = requests.post(self.post_url, data=self.post_data, headers=self._Request_Header)
except Exception as e:
print(e)
else:
if req.status_code == 200:
print(req.text)
list_str = json.loads(req.text).get('list', '')
list_result = self.encrypt_or_decrypt(list_str, False)
# print(list_result)
try:
list_result = json.loads(list_result)
except json.decoder.JSONDecodeError as e:
print(e)
else:
for result in list_result:
self.fetch_detail(result.get('id', ''))

def fetch_detail(self, _id):
content = {'id': _id, 'lb': 'xzcf', 'ztType': ''}
p = self.encrypt_or_decrypt(content, True)
try:
req = requests.post(self.detail_url, data={"p": p}, headers=self._Request_Header)
except Exception as e:
print(e)
else:
if req.status_code == 200:
# print(req.text)
try:
result = json.loads(req.text)
except json.decoder.JSONDecodeError as e:
print(e)
else:
data_list = self.encrypt_or_decrypt(result.get('dataList'), False)
print(data_list)


if __name__ == "__main__":
spider = MySpider()
spider.fetch_list()

上面是完整的代码,先从fetch_list开始看,其中list_result为调用我们的解密服务后获取的内容,通过打印可以看到内容如下(这里仅展示第一条):

1
2
3
4
[
{"xy020103_label":"罚款","zttype":"1","xy020101_label":"洛川县地方税务局石头税务所","xy020101":"洛川县地方税务局石头税务所","xy010101_label":"洛川小柒电子商务有限责任公司","row_id":1,"xy020107_label":"2017-11-28","id":"c22f96112f5e5acfc755ddc42475fa9b","m_010101":0,"xy020102":"洛地税 罚 〔2017〕 2号","xy020107":"2017-11-28","xy020102_label":"洛地税 罚 〔2017...","xy010101":"洛川小柒电子商务有限责任公司","xy020103":"罚款"},
......
]

对比详情页的url=http://credit.shaanxi.gov.cn/queryDetail/xzcf/c22f96112f5e5acfc755ddc42475fa9b.jspx?ztType=,我们找到了关键参数id,但是直接拿id拼接url是无法获取详情页内容得,我们观察网络请求发现,详情页也是一个Post请求,但是请求参数却很奇怪。

1
2
Form Data
p:tYvfZfVux+LKGNA301G7/nlBl3dGy+o46oAO7F/IBy5soTa8wSBX22aIuSgqxoUyLcGjoAese4ay3cq6ndGAp/8oVs4KR81K

4、再次寻找加密的过程

虽然奇怪,但是稍加思考应该就能想到,这或许是对参数又做了加密(正好和我们前面抠出来的BmuY1.go方法对应上了),于是我们采用上面找解密函数同样的步骤,去看它具体对哪些内容做了加密。

发现了一段非常关键的代码:

1
2
3
4
5
6
7
var p = {"id": "c22f96112f5e5acfc755ddc42475fa9b", "lb": "xzcf","ztType":""};
var p1 = BmuY1.go(JSON.stringify(p));
sendPost("/queryItemData.jspx",
{
"p" : p1
},
......

真相大白了,它对{"id": "c22f96112f5e5acfc755ddc42475fa9b", "lb": "xzcf","ztType":""}这个进行了加密,其中id的值就是我们前面获取的关键值。

所以再来看我们的fetch_detail函数就明白了,最后获取到详情页的数据又是一层加密,我们再次调用解密服务就可以了。因为我们要调用多次加密解密服务,所以这里封装成了一个方法encrypt_or_decrypt

至此本次破解就完成了,最后别忘了取消所有断点哦,否则也会影响这个网站下的其他页面的。

附上js服务的server.js代码:

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
47
48
49
'use strict'

const hapi = require('hapi')

// import module
// xinyongshanxi
const xysxHandler = require('./project/xinyongshanxi/shanxi')

//jianyu
const jianyuHandler = require('./project/jianyu/jianyu')

async function init() {
const server = hapi.server({
port: 3001,
host: '192.168.20.141'
})
// route
server.route([
// xinyongshanxi
{
method: 'POST',
path: '/xinyongshanxi',
handler: xysxHandler
},

// jianyu
{
method: 'POST',
path: '/jianyu',
handler: jianyuHandler
},
])
await server.start()
return server
};

process.on('unhandledRejection', err => {
console.log(err)
process.exit(1)
})

init()
.then(server => {
console.log(`Server running on ${server.info.uri}`)
})
.catch(err => {
console.error(`start server failed -> ${err.message}`)
throw err
})