Bootstrap

js逆向-AJAX 请求分析

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));

;