1.准备
本文用到的网址为中国空气质量网2019年03月西安空气质量指数AQI_PM2.5日历史数据_中国空气质量在线监测分析平台历史数据
对于如何抓包,本人上一篇文章有所提及,因为网站会缓存数据,所以每次抓包前要进行数据清空。
2.抓包分析
其中表头没有什么特殊的,但是分析发现负载和相应都是加密数据,然后我们点击发起程序,进入第一个,然后打上断点,清空缓存,然后刷新页面。
3.数据分析
在调用堆栈里面一条条看,发现加密阶段应该是这一部分
下面应该是加密代码
我们多打几个断点,分析出来
var pKmSFk8 = poPBVxzNuafY8Yu(m0fhOhhGL, oBDNNVgaDf)
这一部分应该是对请求部分进行加密,
const dGHdO = dxvERkeEvHbS(response.data);
这一部分是进行解密,我们对此进行分析,打上断点后可以知道m0fhOhhGL固定为"GETDAYDATA",而oBDNNVgaDf这一部分就是 我们相要查询的数据
const ask4u6FbhGV8 = "a0QHmC1Ova5958nC";//AESkey,可自定义
const asi2hhkBUJbo = "bMu71lHRX6bRmPxU";//密钥偏移量IV,可自定义
const acky6QolJSJi = "dLRSzDrm8xkryEyL";//AESkey,可自定义
const acixHVhiNqmK = "fex6AA4zRfVrSPmr";//密钥偏移量IV,可自定义
const dskQCqpdBOGo = "hEaIOlrX7tlhAOkz";//DESkey,可自定义
const dsiqYiQHbZQp = "xMBwDXG1HOubUV04";//密钥偏移量IV,可自定义
const dckCheMkUojW = "oi4aKMxMECWSyTaz";//DESkey,可自定义
const dciEekKS6Cws = "p2uRrSFcN9oKLrKY";//密钥偏移量IV,可自定义
const aes_local_key = 'emhlbnFpcGFsbWtleQ==';
const aes_local_iv = 'emhlbnFpcGFsbWl2';
var BASE64 = {
encrypt: function(text) {
var b = new Base64();
return b.encode(text);
},
decrypt: function(text) {
var b = new Base64();
return b.decode(text);
}
};
var DES = {
encrypt: function(text, key, iv){
var secretkey = (CryptoJS.MD5(key).toString()).substr(0, 16);
var secretiv = (CryptoJS.MD5(iv).toString()).substr(24, 8);
secretkey = CryptoJS.enc.Utf8.parse(secretkey);
secretiv = CryptoJS.enc.Utf8.parse(secretiv);
var result = CryptoJS.DES.encrypt(text, secretkey, {
iv: secretiv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return result.toString();
},
decrypt: function(text, key, iv){
var secretkey = (CryptoJS.MD5(key).toString()).substr(0, 16);
var secretiv = (CryptoJS.MD5(iv).toString()).substr(24, 8);
secretkey = CryptoJS.enc.Utf8.parse(secretkey);
secretiv = CryptoJS.enc.Utf8.parse(secretiv);
var result = CryptoJS.DES.decrypt(text, secretkey, {
iv: secretiv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return result.toString(CryptoJS.enc.Utf8);
}
};
var AES = {
encrypt: function(text, key, iv) {
var secretkey = (CryptoJS.MD5(key).toString()).substr(16, 16);
var secretiv = (CryptoJS.MD5(iv).toString()).substr(0, 16);
// console.log('real key:', secretkey);
// console.log('real iv:', secretiv);
secretkey = CryptoJS.enc.Utf8.parse(secretkey);
secretiv = CryptoJS.enc.Utf8.parse(secretiv);
var result = CryptoJS.AES.encrypt(text, secretkey, {
iv: secretiv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return result.toString();
},
decrypt: function(text, key, iv) {
var secretkey = (CryptoJS.MD5(key).toString()).substr(16, 16);
var secretiv = (CryptoJS.MD5(iv).toString()).substr(0, 16);
secretkey = CryptoJS.enc.Utf8.parse(secretkey);
secretiv = CryptoJS.enc.Utf8.parse(secretiv);
var result = CryptoJS.AES.decrypt(text, secretkey, {
iv: secretiv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return result.toString(CryptoJS.enc.Utf8);
}
};
var localStorageUtil = {
save: function(name, value) {
var text = JSON.stringify(value);
text = BASE64.encrypt(text);
text = AES.encrypt(text, aes_local_key, aes_local_iv);
try {
localStorage.setItem(name, text);
} catch (oException) {
if (oException.name === 'QuotaExceededError') {
console.log('Local limit exceeded');
localStorage.clear();
localStorage.setItem(name, text);
}
}
},
check: function(name) {
return localStorage.getItem(name);
},
getValue: function(name) {
var text = localStorage.getItem(name);
var result = null;
if (text) {
text = AES.decrypt(text, aes_local_key, aes_local_iv);
text = BASE64.decrypt(text);
result = JSON.parse(text);
}
return result;
},
remove: function(name) {
localStorage.removeItem(name);
}
};
// console.log('base64', BASE64.encrypt('key'));
function dU6tPALZ40l(pKmSFk8) {
pKmSFk8 = AES.decrypt(pKmSFk8, ask4u6FbhGV8, asi2hhkBUJbo);
return pKmSFk8;
}
function dqf3PsvG9U(pKmSFk8) {
pKmSFk8 = DES.decrypt(pKmSFk8, dskQCqpdBOGo, dsiqYiQHbZQp);
return pKmSFk8;
}
function golmOACJkTUULBcU(key, period) {
if (typeof period === 'undefined') {
period = 0;
}
var d = DES.encrypt(key);
d = BASE64.encrypt(key);
var data = localStorageUtil.getValue(key);
if (data) { // 判断是否过期
const time = data.time;
const current = new Date().getTime();
if (new Date().getHours() >= 0 && new Date().getHours() < 5 && period > 1) {
period = 1;
}
if (current - (period * 60 * 60 * 1000) > time) { // 更新
data = null;
}
// 防止1-5点用户不打开页面,跨天的情况
if (new Date().getHours() >= 5 && new Date(time).getDate() !== new Date().getDate() && period === 24) {
data = null;
}
}
return data;
}
function osZ34YC04S(obj) {
var newObject = {};
Object.keys(obj).sort().map(function(key){
newObject[key] = obj[key];
});
return newObject;
}
function dxvERkeEvHbS(data) {
data = BASE64.decrypt(data);
data = DES.decrypt(data, dskQCqpdBOGo, dsiqYiQHbZQp);
data = AES.decrypt(data, ask4u6FbhGV8, asi2hhkBUJbo);
data = BASE64.decrypt(data);
return data;
}
var poPBVxzNuafY8Yu = (function(){
function osZ34YC04S(obj){
var newObject = {};
Object.keys(obj).sort().map(function(key){
newObject[key] = obj[key];
});
return newObject;
}
return function(m0fhOhhGL, oNLhNQ){
var aMFs = '3c9208efcfb2f5b843eec8d96de6d48a';
var cVWG2 = 'WEB';
var t5GECZQ = new Date().getTime();
var pKmSFk8 = {
appId: aMFs,
method: m0fhOhhGL,
timestamp: t5GECZQ,
clienttype: cVWG2,
object: oNLhNQ,
secret: hex_md5(aMFs + m0fhOhhGL + t5GECZQ + cVWG2 + JSON.stringify(osZ34YC04S(oNLhNQ)))
};
pKmSFk8 = BASE64.encrypt(JSON.stringify(pKmSFk8));
pKmSFk8 = AES.encrypt(pKmSFk8, acky6QolJSJi, acixHVhiNqmK);
return pKmSFk8;
};
})();
function sSPnfjolBsGjl66hUEw8(m0fhOhhGL, oBDNNVgaDf, cCLupMFJ7, p5kr85Z) {
const k1pT = hex_md5(m0fhOhhGL + JSON.stringify(oBDNNVgaDf));
const dGHdO = golmOACJkTUULBcU(k1pT, p5kr85Z);
if (!dGHdO) {
var pKmSFk8 = poPBVxzNuafY8Yu(m0fhOhhGL, oBDNNVgaDf);
$.ajax({
url: 'api/historyapi.php',
data: { hA4Nse2cT: pKmSFk8 },
type: "post",
success: function (dGHdO) {
dGHdO = dxvERkeEvHbS(dGHdO);
oNLhNQ = JSON.parse(dGHdO);
if (oNLhNQ.success) {
if (p5kr85Z > 0) {
oNLhNQ.result.time = new Date().getTime();
localStorageUtil.save(k1pT, oNLhNQ.result);
}
cCLupMFJ7(oNLhNQ.result);
} else {
console.log(oNLhNQ.errcode, oNLhNQ.errmsg);
}
}
});
} else {
cCLupMFJ7(dGHdO);
}
}
请求加密
我们先分析请求是如何加密的
return function(m0fhOhhGL, oNLhNQ){
var aMFs = '3c9208efcfb2f5b843eec8d96de6d48a';
var cVWG2 = 'WEB';
var t5GECZQ = new Date().getTime();
var pKmSFk8 = {
appId: aMFs,
method: m0fhOhhGL,
timestamp: t5GECZQ,
clienttype: cVWG2,
object: oNLhNQ,
secret: hex_md5(aMFs + m0fhOhhGL + t5GECZQ + cVWG2 + JSON.stringify(osZ34YC04S(oNLhNQ)))
};
pKmSFk8 = BASE64.encrypt(JSON.stringify(pKmSFk8));
pKmSFk8 = AES.encrypt(pKmSFk8, acky6QolJSJi, acixHVhiNqmK);
return pKmSFk8;
};
})();
上面涉及到MD5 哈希加密和 AES 对称加密,同时还使用了 Base64 编码,没有进行魔改,上面有密匙
响应加密
function dxvERkeEvHbS(data) {
data = BASE64.decrypt(data);
data = DES.decrypt(data, dskQCqpdBOGo, dsiqYiQHbZQp);
data = AES.decrypt(data, ask4u6FbhGV8, asi2hhkBUJbo);
data = BASE64.decrypt(data);
return data;
}
这个也是很正常的解密。
4.结果,我的代码
python部分
import requests
import base64
from Crypto.Cipher import DES, AES
from Crypto.Util.Padding import pad, unpad
import hashlib
import re
import json
import execjs
# 密钥和偏移量定义
ask4u6FbhGV8 = "a0QHmC1Ova5958nC"
asi2hhkBUJbo = "bMu71lHRX6bRmPxU"
acky6QolJSJi = "dLRSzDrm8xkryEyL"
acixHVhiNqmK = "fex6AA4zRfVrSPmr"
dskQCqpdBOGo = "hEaIOlrX7tlhAOkz"
dsiqYiQHbZQp = "xMBwDXG1HOubUV04"
dckCheMkUojW = "oi4aKMxMECWSyTaz"
dciEekKS6Cws = "p2uRrSFcN9oKLrKY"
aes_local_key = 'emhlbnFpcGFsbWtleQ=='
aes_local_iv = 'emhlbnFpcGFsbWl2'
def md5_hex(key):
return hashlib.md5(key.encode()).hexdigest()
def base64_encrypt(text):
return base64.b64encode(text.encode()).decode()
def base64_decrypt(text):
# 去除非 Base64 字符
text = re.sub(r'[^A-Za-z0-9+/=]', '', text)
# 填充 Base64 字符串,使其长度为 4 的倍数
missing_padding = len(text) % 4
if missing_padding:
text += '=' * (4 - missing_padding)
return base64.b64decode(text).decode()
def des_encrypt(text, key, iv):
# 取 md5 哈希值的前 8 个字符作为 DES 密钥,确保长度为 8 字节
secretkey = md5_hex(key)[:8].encode()
# 取 md5 哈希值的后 8 个字符作为 DES 初始化向量,确保长度为 8 字节
secretiv = md5_hex(iv)[-8:].encode()
cipher = DES.new(secretkey, DES.MODE_CBC, secretiv)
padded_text = pad(text.encode(), DES.block_size)
encrypted_text = cipher.encrypt(padded_text)
return base64.b64encode(encrypted_text).decode()
def des_decrypt(text, key, iv):
# 取 md5 哈希值的前 8 个字符作为 DES 密钥,确保长度为 8 字节
secretkey = md5_hex(key)[:8].encode()
# 取 md5 哈希值的后 8 个字符作为 DES 初始化向量,确保长度为 8 字节
secretiv = md5_hex(iv)[-8:].encode()
ciphertext = base64.b64decode(text)
cipher = DES.new(secretkey, DES.MODE_CBC, secretiv)
decrypted_text = cipher.decrypt(ciphertext)
return unpad(decrypted_text, DES.block_size).decode()
def aes_encrypt(text, key, iv):
secretkey = md5_hex(key)[16:].encode()
secretiv = md5_hex(iv)[:16].encode()
cipher = AES.new(secretkey, AES.MODE_CBC, secretiv)
padded_text = pad(text.encode(), AES.block_size)
encrypted_text = cipher.encrypt(padded_text)
return base64.b64encode(encrypted_text).decode()
def aes_decrypt(text, key, iv):
secretkey = md5_hex(key)[16:].encode()
secretiv = md5_hex(iv)[:16].encode()
ciphertext = base64.b64decode(text)
cipher = AES.new(secretkey, AES.MODE_CBC, secretiv)
decrypted_text = cipher.decrypt(ciphertext)
return unpad(decrypted_text, AES.block_size).decode()
def dxvERkeEvHbS(data):
data = base64_decrypt(data)
data = des_decrypt(data, dskQCqpdBOGo, dsiqYiQHbZQp)
data = aes_decrypt(data, ask4u6FbhGV8, asi2hhkBUJbo)
data = base64_decrypt(data)
return data
def decryptPoPBVxzNuafY8Yu(encryptedData):
# 1. AES 解密
aesDecrypted = aes_decrypt(encryptedData, acky6QolJSJi, acixHVhiNqmK)
# 2. Base64 解密
base64Decrypted = base64_decrypt(aesDecrypted)
# 3. 解析为 JSON 对象
return json.loads(base64Decrypted)
def hex_md5(s):
return hashlib.md5(s.encode('utf-8')).hexdigest()
def BASE64_encrypt(text):
return base64.b64encode(text.encode('utf-8')).decode('utf-8')
def AES_encrypt(text, key, iv):
secretkey = hex_md5(key)[16:].encode('utf-8')
secretiv = hex_md5(iv)[:16].encode('utf-8')
cipher = AES.new(secretkey, AES.MODE_CBC, secretiv)
padded_text = pad(text.encode('utf-8'), AES.block_size)
encrypted_text = cipher.encrypt(padded_text)
return base64.b64encode(encrypted_text).decode('utf-8')
city = {'city': '长春', 'month': '201612'}
det = 'GETDAYDATA'
with open('aes.js', 'r', encoding='utf-8') as file:
js_code = file.read()
# 创建一个 JavaScript 上下文
ctx = execjs.compile(js_code)
# 调用 hex_md5 函数
result = ctx.call('poPBVxzNuafY8Yu',det,city)# 这一步是数据加密
#print("加密结果:", result)
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36 Edg/133.0.0.0",
}
url = 'https://www.aqistudy.cn/historydata/api/historyapi.php'
data = {
"hA4Nse2cT": result
}#挟带密文去请求参数
try:
r = requests.post(url, headers=headers, data=data)
r.encoding = 'utf-8' # 明确指定字符编码
r.raise_for_status() # 检查请求是否成功
#print(r.text) # 打印返回的响应文本
# 调用解密函数,传递响应文本
result = dxvERkeEvHbS(r.text)#模仿js代码去对数据进行解密
print(result)
except requests.RequestException as e:
print(f"请求网络时出错: {e}")
except Exception as e:
print(f"解密过程中出错: {e}")
js部分
// 假设引入了 CryptoJS 和 Base64 库
const CryptoJS = require('crypto-js');
const Base64 = require('js-base64').Base64;
const ask4u6FbhGV8 = "a0QHmC1Ova5958nC";// AESkey,可自定义
const asi2hhkBUJbo = "bMu71lHRX6bRmPxU";// 密钥偏移量IV,可自定义
const acky6QolJSJi = "dLRSzDrm8xkryEyL";// AESkey,可自定义
const acixHVhiNqmK = "fex6AA4zRfVrSPmr";// 密钥偏移量IV,可自定义
const dskQCqpdBOGo = "hEaIOlrX7tlhAOkz";// DESkey,可自定义
const dsiqYiQHbZQp = "xMBwDXG1HOubUV04";// 密钥偏移量IV,可自定义
const dckCheMkUojW = "oi4aKMxMECWSyTaz";// DESkey,可自定义
const dciEekKS6Cws = "p2uRrSFcN9oKLrKY";// 密钥偏移量IV,可自定义
const aes_local_key = 'emhlbnFpcGFsbWtleQ==';
const aes_local_iv = 'emhlbnFpcGFsbWl2';
var BASE64 = {
encrypt: function(text) {
return Base64.encode(text);
},
decrypt: function(text) {
return Base64.decode(text);
}
};
var DES = {
encrypt: function(text, key, iv){
var secretkey = (CryptoJS.MD5(key).toString()).substr(0, 16);
var secretiv = (CryptoJS.MD5(iv).toString()).substr(24, 8);
secretkey = CryptoJS.enc.Utf8.parse(secretkey);
secretiv = CryptoJS.enc.Utf8.parse(secretiv);
var result = CryptoJS.DES.encrypt(text, secretkey, {
iv: secretiv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return result.toString();
},
decrypt: function(text, key, iv){
var secretkey = (CryptoJS.MD5(key).toString()).substr(0, 16);
var secretiv = (CryptoJS.MD5(iv).toString()).substr(24, 8);
secretkey = CryptoJS.enc.Utf8.parse(secretkey);
secretiv = CryptoJS.enc.Utf8.parse(secretiv);
var result = CryptoJS.DES.decrypt(text, secretkey, {
iv: secretiv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return result.toString(CryptoJS.enc.Utf8);
}
};
var AES = {
encrypt: function(text, key, iv) {
var secretkey = (CryptoJS.MD5(key).toString()).substr(16, 16);
var secretiv = (CryptoJS.MD5(iv).toString()).substr(0, 16);
secretkey = CryptoJS.enc.Utf8.parse(secretkey);
secretiv = CryptoJS.enc.Utf8.parse(secretiv);
var result = CryptoJS.AES.encrypt(text, secretkey, {
iv: secretiv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return result.toString();
},
decrypt: function(text, key, iv) {
var secretkey = (CryptoJS.MD5(key).toString()).substr(16, 16);
var secretiv = (CryptoJS.MD5(iv).toString()).substr(0, 16);
secretkey = CryptoJS.enc.Utf8.parse(secretkey);
secretiv = CryptoJS.enc.Utf8.parse(secretiv);
var result = CryptoJS.AES.decrypt(text, secretkey, {
iv: secretiv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return result.toString(CryptoJS.enc.Utf8);
}
};
// 这里假设 hex_md5 函数的实现
function hex_md5(s) {
return CryptoJS.MD5(s).toString();
}
var localStorageUtil = {
save: function(name, value) {
var text = JSON.stringify(value);
text = BASE64.encrypt(text);
text = AES.encrypt(text, aes_local_key, aes_local_iv);
try {
localStorage.setItem(name, text);
} catch (oException) {
if (oException.name === 'QuotaExceededError') {
console.log('Local limit exceeded');
localStorage.clear();
localStorage.setItem(name, text);
}
}
},
check: function(name) {
return localStorage.getItem(name);
},
getValue: function(name) {
var text = localStorage.getItem(name);
var result = null;
if (text) {
text = AES.decrypt(text, aes_local_key, aes_local_iv);
text = BASE64.decrypt(text);
result = JSON.parse(text);
}
return result;
},
remove: function(name) {
localStorage.removeItem(name);
}
};
function dU6tPALZ40l(pKmSFk8) {
pKmSFk8 = AES.decrypt(pKmSFk8, ask4u6FbhGV8, asi2hhkBUJbo);
return pKmSFk8;
}
function dqf3PsvG9U(pKmSFk8) {
pKmSFk8 = DES.decrypt(pKmSFk8, dskQCqpdBOGo, dsiqYiQHbZQp);
return pKmSFk8;
}
function golmOACJkTUULBcU(key, period) {
if (typeof period === 'undefined') {
period = 0;
}
var d = DES.encrypt(key);
d = BASE64.encrypt(key);
var data = localStorageUtil.getValue(key);
if (data) { // 判断是否过期
const time = data.time;
const current = new Date().getTime();
if (new Date().getHours() >= 0 && new Date().getHours() < 5 && period > 1) {
period = 1;
}
if (current - (period * 60 * 60 * 1000) > time) { // 更新
data = null;
}
// 防止1-5点用户不打开页面,跨天的情况
if (new Date().getHours() >= 5 && new Date(time).getDate() !== new Date().getDate() && period === 24) {
data = null;
}
}
return data;
}
function osZ34YC04S(obj) {
var newObject = {};
Object.keys(obj).sort().forEach(function(key){
newObject[key] = obj[key];
});
return newObject;
}
function dxvERkeEvHbS(data) {
data = BASE64.decrypt(data);
data = DES.decrypt(data, dskQCqpdBOGo, dsiqYiQHbZQp);
data = AES.decrypt(data, ask4u6FbhGV8, asi2hhkBUJbo);
data = BASE64.decrypt(data);
return data;
}
var poPBVxzNuafY8Yu = (function(){
function osZ34YC04S(obj){
var newObject = {};
Object.keys(obj).sort().forEach(function(key){
newObject[key] = obj[key];
});
return newObject;
}
return function poPBVxzNuafY8Yu(m0fhOhhGL, oNLhNQ){
var aMFs = '3c9208efcfb2f5b843eec8d96de6d48a';
var cVWG2 = 'WEB';
var t5GECZQ = new Date().getTime();
var pKmSFk8 = {
appId: aMFs,
method: m0fhOhhGL,
timestamp: t5GECZQ,
clienttype: cVWG2,
object: oNLhNQ,
secret: hex_md5(aMFs + m0fhOhhGL + t5GECZQ + cVWG2 + JSON.stringify(osZ34YC04S(oNLhNQ)))
};
pKmSFk8 = BASE64.encrypt(JSON.stringify(pKmSFk8));
pKmSFk8 = AES.encrypt(pKmSFk8, acky6QolJSJi, acixHVhiNqmK);
return pKmSFk8;
};
})();
let m0fhOhhGL = 'GETDAYDATA';
let oNLhNQ = {'city': '西安', 'month': '201612'};
console.log(poPBVxzNuafY8Yu(m0fhOhhGL, oNLhNQ));