一、数据接口分析
主页地址:某鸟记录中心
1、抓包
通过抓包可以发现数据接口是front/record/search/page
2、判断是否有加密参数
- 请求参数是否加密?
通过查看“载荷”模块可以发现,请求参数是加密的
- 请求头是否加密?
通过查看“请求标头”可以发现Requestid
和Sign
是加密参数,并且有一个Timestamp
时间戳
- 响应是否加密?
通过查看“响应”模块可以发现,响应中的数据是加密数据
- cookie是否加密?
无
二、加密位置定位
1、加密参数以及请求头加密
(1)看启动器
查看启动器发现里面有一个pullData的调用堆栈,点进去查看
点进去后,可以看出,此处是发送ajax请求的位置,在此处下断点,再次翻页获取数据,发现可以断住,但是此处是明文数据,所以加密位置不在这里。
(2)搜索关键字
因为“载荷”是一整个密文,没有关键字,所以无法搜索
(3)hook
因为“载荷”是一整个密文,所以网站大概率会使用JSON.stringify
将数据转换为json字符串再进行加密,所以我们可以hookJSON.stringify
,hook代码:
var my_stringify = JSON.stringify;
JSON.stringify = function (params) {
debugger
console.log("json_stringify params:",params);
return my_stringify(params);
};
运行hook代码,再次获取数据,发现可以断住
继续调试执行,可以发现,网站是使用ajaxSetup
设置了ajax请求的全局配置,请求头和加密参数都是在此处进行赋值的。
2、响应数据
(1)hook
因为响应加密数据一般都是json数据加密,所以解密后会使用JSON.parse
进行解密,所以我们可以对JSON.parse
进行hook
hook代码段:
var my_parse = JSON.parse;
JSON.parse = function (params) {
debugger
console.log("json_parse params:",params);
return my_parse(params);
};
运行hook代码,再次获取数据,发现可以断住明文
接着调试执行,就可以找到解密位置
三、扣js代码
1、加密参数及请求头
将定位到的加密位置的代码扣出,可以发现网站的数据加密是使用的JSEncrypt
模块进行的RAS加密,所以我们可以直接使用标准模块进行加密,但是网站使用的encrypt.encryptUnicodeLong
方法,node.js中是没有的,所以我们还需要将这个方法扣出来。下方请求头加密使用的md5,我们同样可以使用标准模块进行加密。
2、响应数据
将定位到的解密位置的代码扣出,进入到网站的解密方法BIRDREPORT_APIJS.decode
中,可以发现,网站是使用的AES解密的,所以我们可以使用标准的AES模块进行解密。
JavaScript源码:
const JSEncrypt = require('jsencrypt');
var b64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var b64pad = "=";
function hex2b64(h) {
var i;
var c;
var ret = "";
for (i = 0; i + 3 <= h.length; i += 3) {
c = parseInt(h.substring(i, i + 3), 16);
ret += b64map.charAt(c >> 6) + b64map.charAt(c & 63);
}
if (i + 1 == h.length) {
c = parseInt(h.substring(i, i + 1), 16);
ret += b64map.charAt(c << 2);
} else if (i + 2 == h.length) {
c = parseInt(h.substring(i, i + 2), 16);
ret += b64map.charAt(c >> 2) + b64map.charAt((c & 3) << 4);
}
while ((ret.length & 3) > 0) {
ret += b64pad;
}
return ret;
}
JSEncrypt.prototype.encryptUnicodeLong = function (string) {
var k = this.getKey();
//根据key所能编码的最大长度来定分段长度。key size - 11:11字节随机padding使每次加密结果都不同。
var maxLength = ((k.n.bitLength() + 7) >> 3) - 11;
var subStr = "", encryptedString = "";
var subStart = 0, subEnd = 0;
var bitLen = 0, tmpPoint = 0;
for (var i = 0, len = string.length; i < len; i++) {
//js 是使用 Unicode 编码的,每个字符所占用的字节数不同
var charCode = string.charCodeAt(i);
if (charCode <= 0x007f) {
bitLen += 1;
} else if (charCode <= 0x07ff) {
bitLen += 2;
} else if (charCode <= 0xffff) {
bitLen += 3;
} else {
bitLen += 4;
}
//字节数到达上限,获取子字符串加密并追加到总字符串后。更新下一个字符串起始位置及字节计算。
if (bitLen > maxLength) {
subStr = string.substring(subStart, subEnd)
encryptedString += k.encrypt(subStr);
subStart = subEnd;
bitLen = bitLen - tmpPoint;
} else {
subEnd = i;
tmpPoint = bitLen;
}
}
subStr = string.substring(subStart, len)
encryptedString += k.encrypt(subStr);
return hex2b64(encryptedString);
};
const CryptoJS = require('crypto-js')
function getUuid() {
var s = [];
var a = "0123456789abcdef";
for (var i = 0; i < 32; i++) {
s[i] = a.substr(Math.floor(Math.random() * 0x10), 1)
}
s[14] = "4";
s[19] = a.substr((s[19] & 0x3) | 0x8, 1);
s[8] = s[13] = s[18] = s[23];
var b = s.join("");
return b
}
function sort_ASCII(a) {
var b = new Array();
var c = 0;
for (var i in a) {
b[c] = i;
c++
}
var d = b.sort();
var e = {};
for (var i in d) {
e[d[i]] = a[d[i]]
}
return e
}
function url2json(a) {
var b = /^[^\?]+\?([\w\W]+)$/
, reg_para = /([^&=]+)=([\w\W]*?)(&|$|#)/g
, arr_url = b.exec(a)
, ret = {};
if (arr_url && arr_url[1]) {
var c = arr_url[1], result;
while ((result = reg_para.exec(c)) != null) {
ret[result[1]] = result[2]
}
}
return ret
}
function dataTojson(a) {
var b = [];
var c = {};
b = a.split('&');
for (var i = 0; i < b.length; i++) {
if (b[i].indexOf('=') != -1) {
var d = b[i].split('=');
if (d.length == 2) {
c[d[0]] = d[1]
} else {
c[d[0]] = ""
}
} else {
c[b[i]] = ''
}
}
return c
}
const serialize = function (a) {
var b = [];
for (var p in a)
if (a.hasOwnProperty(p) && a[p]) {
b.push(encodeURIComponent(p) + '=' + encodeURIComponent(a[p]))
}
return b.join('&')
};
var paramPublicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCvxXa98E1uWXnBzXkS2yHUfnBM6n3PCwLdfIox03T91joBvjtoDqiQ5x3tTOfpHs3LtiqMMEafls6b0YWtgB1dse1W5m+FpeusVkCOkQxB4SZDH6tuerIknnmB/Hsq5wgEkIvO5Pff9biig6AyoAkdWpSek/1/B7zYIepYY0lxKQIDAQAB";
var encrypt = new JSEncrypt();
encrypt.setPublicKey(paramPublicKey);
function get_params() {
var b_data = 'page=1&limit=20&taxonid=&startTime=&endTime=&province=&city=&district=&pointname=%E6%B2%B3%E5%8D%97&username=&serial_id=&ctime=&taxonname=&state=&mode=0&outside_type=0'
var c = Date.parse(new Date());
var d = getUuid();
var e = JSON.stringify(sort_ASCII(dataTojson(b_data || '{}')));
b_data = encrypt.encryptUnicodeLong(e);
var f = CryptoJS.MD5(e + d + c).toString();
return [{
timestamp: c.toString(),
requestId: d.toString(),
sign: f.toString()
}, b_data]
}
var key = '3583ec0257e2f4c8195eec7410ff1619';
var iv = 'd93c0d5ec6352f20'
apijs_decode = function(a) {
var b = CryptoJS.enc.Utf8.parse(key);
var c = CryptoJS.enc.Utf8.parse(iv);
var d = CryptoJS.AES.decrypt(a, b, {
iv: c,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return d.toString(CryptoJS.enc.Utf8)
}
function get_data(r_data) {
var decode_str = apijs_decode(r_data);
return JSON.parse(decode_str)
}