中国某矿采购平台逆向
中国某矿采购平台逆向
传送门 :aHR0cHM6Ly9lYy5taW5tZXRhbHMuY29tLmNuL29wZW4vaG9tZS9wdXJjaGFzZS1pbmZvLz90YWJJbmRleD0w

我们首先进网站,看到网页结构是做了分类和搜索的,基本由此判断这边的数据是ajax异步过来的,
查询条件用post请求发过去的
接下来我们进f12,简单抓一下包,看看发过去的数据是明文还是加密的

这里可以看到,我们正常翻页后抓到了2个xhr请求,一个是public,一个是by-lx-page
点开之后可以看到只有一个是正确返回结果的请求

public这个请求返回的一串乱码,我们先不管

点开另外一个请求之后发现是一个json数据传过去,同时里面param键对应的值是加密的状态
接下来我们尝试hook一下这个xhr请求

这里用到了开源的hook神器v_jstools hook到请求发起时就可以
经过一系列的查找后定位到一个叫setKey的函数

在收到public请求的返回值后直接就调用到了这个函数,根据下面的代码可以得知这应该是js经典的CryptoJS库的内置方法,用于设置加密对象的key,这个具体的key是什么类型的,我们继续往下看
我们打个断点到调用这个方法之前的一个函数setPublickey

看到这个e对应的就是public返回的值,同时这个cryptojs内置方法对应的setPublickey就是设置公钥的意思,按照这个往下推基本就可以得知,在发起真正请求到页面数据前,网站都会发起一个请求,向服务器请求一串加密参数的公钥,用于接下来的加密
接下来我们继续找具体加密的位置
我们直接全局搜索setPublickey这个函数在哪里被调用了

哦吼,看到这基本就明了了,a = 翻页需要的数据,这个a在经过RSA加密前还有几个参数,这个sign字段是在整体数据加密前又用f 函数 加密得到的值, timestamp这个字段就很好了解,就是个js的标准13位时间戳
接下来我们一步一步往下单步调试
首先到定到 f 这个函数上,我们直接控制台输出一下f函数的具体内容

发现调用的其实就是CryptoJS库里的MD5哈希方法
然后我们再来往下看encryptlong 函数的具体作用

在这里可以看到传入的是加密前的查询参数的值
{"inviteMethod": "", "businessClassfication": "", "mc": "", "lx": "ZBGS", "dwmc": "","pageIndex": 1}
这里是初始的查询参数,后续在a的生成方法里又制定了其他2个参数的生成,所以最后到encryptlong
这个函数时,A的值应该是这样
{'inviteMethod': '', 'businessClassfication': '', 'mc': '', 'lx': 'ZBGS', 'dwmc': '', 'pageIndex': 1, 'sign': '2d9c78743e64e29ca51cee56f4490430', 'timeStamp': 1682169781298}
其中sign对应的就是查询参数初始json送入MD5加密后的值
接下来我们回到encryptlong 方法

发现这里获取了公钥对象之后,对公钥长度进行了一个位运算,原因是RSA公钥加密对加密内容的长度是有限制的,这个长度是 (公钥位长 + 7 然后向右移3位) – 11 这一段在python中也可以对应的实现
key = RSA.importKey(rsa_key) # 导入公钥
cipher = PKCS1_cipher.new(key) # 创建新公钥对象
long = (key.size_in_bits() + 7 >> 3) - 11 # 获取位长然后做后续运算
然后我们可以得出这个RSA公钥可加密最大位为117,我们所需要加密的内容远大于这个,所以这里做了一个循环分每50个字符串切片一次,然后送去RSA加密,最后再拼接这个结果
49d557a4b74ea2909b059b6bef2be2ff1f59060782df26a742135bb8f730c281fdbb847b7b8588898bee3c23bbb8510ffcf8ac8b829e1a7e9d8d571993cf356dbbf9c206549f000fc994c82f7ec038391dfa63be655d31e546b772b11edd9dbb039520a48138e17eb96fe1610e81706216e8e694f8892cefcbf678061f25059f200219dc9aa54b336607adf49fcb71eb0d930cfee7ecdbc8870c18893439bb0e5df1d80ffe487d8eea8ecf18a20d9af9c8f218a7eeba8bc35d18f630dda720d46f2487821a474c29dce0a7864d75926c370528a27fcce9ffe034fc682741456147b49b3e1e12dbaabd5f500d405ad681c6c8c68d96349a87d458fcbc06d6740f561fa9c86c6a6260596f0377146f5f4100965a62198a5c10a00e5247d1d83d8db228aebd89be757520ffeab8de1815d5d83889a5c14ff0ee5624b9ba967fc171b2bb602e94471c69f11aa822cedf7e4cf3e8470649bee1463721efabeb3c5672bac4e0c8cab69468831769fbb2d1920ee8142f93ea266fde4eaa6499c6ed3d55301a5c79b2089663ca7624b447d2949603f912506d6f4eea836166e122d98b5bfd2c227f10a76ce4d208c6ec391cd1d4a7061592b62b4ffb4e1621e6b2b858f7176360c6713f40f07176d1ae9a2235c33f44cab0d11918162c807d35138bad9511cb24dc02d59ddedc9e970535a69fc59a3e8392d8b9a3a8c409b474d351386e
我们可以看到这一串结果非常长,而且是16进制的,跟我们一开始抓包得到的param的值并不一样
所以这一串并不是最后加密的结果 p.s CryptoJS 中默认返回的结果是16进制
往后看返回借过前还进入了一个函数 w, 由此可见我们最后想要的结果应该就是这个w 函数中生成的,我们打个断点进来看看

这里注意到一个很关键的问题就是这个条件判断,只有需要加密内容的长度大于RSA公钥可加密长度时,才会进入这个循环,但是他们最后都指向了w 所以我们不需要太关心

我们现在进入w这个方法具体看一下加密的内容,首先我们先看r,a 这两个变量,可以看到我们一开始抓到的param里面就是由这里的字符组成的,然后往后继续看这个for循环,可以看到里面循环的条件是 3个3个循环,同时循环体内t变量是substring方法切片需要加密内容,同时外面包着parseint方法
我们知道parseint方法通常来讲就是把字符串转换为数字,而第二个值就是他的基数,简单的来说这一段代码就是将需要加密内容3/3切片,然后逐个将取到的切片按16进制转换为整数,再将这个整数做一个位运算向右6位,再用charat方法取到加密字符r的第几位,再加上63按位与运算的结果放到加密字符串取下标,由此我们可以用python来实现一下这个算法
def encode_params(data_: str):
key1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
key2 = '='
gen_key = ''
for i in range(0, len(data_), 3):
if i + 3 <= len(data_):
iv = int(data_[i:i + 3], 16)
sign = key1[iv >> 6] + key1[63 & iv]
gen_key += sign
g = int(data_[-1], 16)
gen_key += key1[g << 2]
while 3 & len(gen_key):
gen_key += key2
return gen_key
到此整个逆向完成了,我们综合一下代码可以得到以下结果
# ras长字符串加密
def rsa_long_encrypt(rsa_key: str, data_: str or dict):
if isinstance(data_, dict):
# 字典转字符串
data_ = json.dumps(data_).replace(' ', '')
# 实例化RSA公钥对象
key = RSA.importKey(rsa_key)
cipher = PKCS1_cipher.new(key)
# 计算publickey 可加密位长
long = (key.size_in_bits() + 7 >> 3) - 11
result = ''
# 如果超出,则循环加密,否则直接加密
if len(data_) > long:
for index in range(0, len(data_), 50):
result += cipher.encrypt(data_[index:index + 50].encode()).hex()
return result
return cipher.encrypt(data_.encode()).hex()
def encode_params(data_: str):
# 第二次加密内容
key1 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
key2 = '='
gen_key = ''
for i in range(0, len(data_), 3):
# 判断有效迭代次数
if i + 3 <= len(data_):
iv = int(data_[i:i + 3], 16)
sign = key1[iv >> 6] + key1[63 & iv]
gen_key += sign
# 最后加上一个标识符
g = int(data_[-1], 16)
gen_key += key1[g << 2]
# 循环向加密结果后添加key2,如果长度不满足条件
while 3 & len(gen_key):
gen_key += key2
return gen_key
def decode_js(rsa_key, data_: dict):
data = data_.copy()
# 先填充sign和时间戳
data['sign'] = hashlib.md5(json.dumps(data).replace(' ', '').encode()).hexdigest()
# 13位时间戳
data['timeStamp'] = round(time.time() * 1000)
encrypt = rsa_long_encrypt(rsa_key, data)
signal = encode_params(encrypt)
return signal

我们试一下,完美运行
现在总结一下需要注意的点
1.加密初始字符串的时候需要判断一下长度,如果查询内容过长则需要循环加密
2.RSA公钥需要动态的获取,每次都会变
3.请求接口返回的RSA公钥并没有标准的定位字符串所以要手动添加
rsa_key = "XXXXXXXX"
rsa_key = "-----BEGIN PUBLIC KEY-----\n" + rsa_key + "\n-----END PUBLIC KEY-----"
4.使用json.dumps把初始需要加密的参数转换为字符串时需要注意去空格

