1. 逆向js-破解翻译网页的爬取
-
- 分析目标:某词霸的PC端
-
- 爬取目的:制作属于自己的翻译小词典
2. 抓包分析
选中网页右击点‘检查’或者F12, 选择network, 输入“中国”, 回车
查看请求的参数:
查看实际返回的翻译结果:
3.解决方案
-
- 第一种方案:使用模拟浏览器类似selenium的工具, 绕过js加解密直接获取
-
- 第二种方案:逆向js
3.1 逆向请求参数sign加密
从network抓包请求来看, 这是一个ajax请求(XMLHttpRequest),返回的是json数据。 我们需要通过打断点来定位sign的加密过程。
选中Source, 最右侧面板的 XHR/fetch Breakpoints, 添加"index.php"
添加之后, 重新输入需要翻译的词语, 回车,断点就开始生效了:
重新输入单词,即会断住,可以看到,此时 sign 参数已经生成了:
// 发送的请求全网址
"/index.php?c=trans&m=fy&client=6&auth_user=key_web_new_fanyi&sign=".concat(encodeURIComponent(r))
找到R值是最为关键
// 其中 r 值 是需要特殊关注的
var r = u()("6key_web_new_fanyi".concat(s.LI).concat(t.q.replace(/(^\s*)|(\s*$)/g, ""))).toString().substring(0, 16);
return r = (0, _.$Q)(r),
// 1. "6key_web_new_fanyi" 固定字符串(是否是固定需要多次测试)
// 2. s.LI 固定字符串:“6dVjYLFyzfkFkk”
// 3. t.q 是需要翻译的数据:“中国”
// 4. concat()函数是字符串拼接方法,
// replace() 是字符串替换方法,
// toString() 是转换成字符串的类型
// substring(0,16) 截取前 16个字符
//
一直往下点击追踪,会发现 u() 方法内部使用的是:MD5加密
var r = u()("6key_web_new_fanyi".concat(s.LI).concat(t.q.replace(/(^\s*)|(\s*$)/g, ""))).toString().substring(0, 16);
// 所以从上面的 追踪结果来分析的结论是:
MD5("6key_web_new_fanyi" + “6dVjYLFyzfkFkk” + “中国”)[:16]
将拼接的字符创 使用MD5加密, 加密之后是32位, 截取前 16
断点继续
return r = (0, _.$Q)(r),
进入内部方法
从以上代码中可以 看出是AES加密,加密mode为ECB,padding为 PKCS7,
使用过AES加密的人知道AES需要一个key来进行加密,所以我们需要找到key的值
n.AES.encrypt(e, r, {
mode: n.mode.ECB,
padding: n.pad.Pkcs7}
// e 是需要加密的 内容
// r 就是加密的key
r = n.enc.Utf8.parse(s(t))
// r 是经过 s(t) 函数来获取的, n.enc.utf8.parse只是在转译
// key: 'L4fBtD5fLC9FQw22'
分析到这一步, sign值的加密过程就分析完毕了, 我们可以根据实现原理通过Python来实现并验证
// aes_secret.py
import base64
import hashlib
import urllib.parse
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
# AES 加密方法
def aes_encrypt(aes_key: str, text: str) -> str:
key = aes_key.encode('utf-8')
srcs = text.encode('utf-8')
cipher = AES.new(key, AES.MODE_ECB)
# 在加密之前进行填充
padded_data = pad(srcs, AES.block_size)
encrypted = cipher.encrypt(padded_data)
# 返回 base64 编码后的密文
return base64.b64encode(encrypted).decode('utf-8')
# 生成Sign的方法
def get_sign() -> str:
search_word = '中国'
aes_key = 'L4fBtD5fLC9FQw22'
concat_search_params = '6key_web_new_fanyi' + '6dVjYLFyzfkFkk' + search_word
md5_encrypt_result = hashlib.md5(concat_search_params.encode()).hexdigest()[:16]
sign = aes_encrypt(aes_key, md5_encrypt_result)
return urllib.parse.quote(sign)
print(get_sign())
# aG5ry6oF+IvstLPiNdhDiNRriqVIAJSQ+xmfU0q7dIE=
# 经过对比 这个加密值 和 爱词霸的sign 一模一样
3.2 逆向 content 的加密内容
在这里解密的方法是:B6
进入B6 之后可以看到解密也使用的市AES:
解密方式:AES
key: aahc3TfyfCEmER33
mode:ECB
padd:pkcs7
分析完解密js之后, python复现解密过程
# aes_secret.py
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
def aes_decrypt(ciphertext):
key = 'aahc3TfyfCEmER33'.encode('utf-8')
# 将密文进行 base64 解码
ciphertext = base64.b64decode(ciphertext)
# 创建 AES 解密器对象
cipher = AES.new(key, AES.MODE_ECB)
# 对密文进行解密
decrypted_data = cipher.decrypt(ciphertext)
# 对解密后的数据进行去填充操作
decrypted_data = unpad(decrypted_data, AES.block_size)
# 返回解密后的明文
return decrypted_data.decode('utf-8')
content = "nCRjxZwyh/7P3ee74BIU0r+VpYqC1/VewPqzx0un7jyu16Zu+DoJ69fUH5j/kw8Qy0B+M69qfiZXuNmK/ojCoUt/Ccnr8yD8+ptxnH1DkEEsTfbrhTNWIXzg5vsxTi8raJf9MJpvANQw7285y3YD7L9fWOC9Nn0hsLdDAWJOgm4p45ozDcMrAvaLI5kVnkC5DXmu4lgquGouf1F+k4YFpiEUJ5XHAbshY51gmoeoEXYuobI4eswavxrdxqh0HYSXNLxoz198exBfVyR3fSa2eUGRQWYe1P8xK/dskvuEYXw0RbGM0yYu02AzQ2wHI+0PzR1yzSbJQp1MiZ5Y8eisCt9UjVIme7ZpNyfr3u8U3sg="
print(aes_decrypt(content))
# china
4. 完整代码过程
注意: aes_secret.py 需要自己创建
import requests
import json
from aes_secrect import aes_decrypt,get_sign
class King(object):
def __init__(self, word):
self.word = word
# 生成加密的sign
sign = get_sign(self.word)
print('sign:', sign)
self.url = f"https://ifanyi.iciba.com/index.php?c=trans&m=fy&client=6&auth_user=key_web_new_fanyi&sign={sign}"
self.headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36"
}
self.post_data = {
"from": "auto",
"to": "auto",
"q": self.word
}
def get_data(self):
response = requests.post(self.url, headers=self.headers, data=self.post_data)
# 解密 返回的加密内容
data = aes_decrypt(response.json()['content'])
return data
def parse_data(self, data):
# 打印解密之后的数据
print(data)
# 将json数据转换成python字典
dict_data = json.loads(data)
# 从字典中抽取翻译结果
try:
print(dict_data['out'])
except:
print("翻译失败")
def run(self):
# 发送请求
data = self.get_data()
# 解析
self.parse_data(data)
if __name__ == '__main__':
king = King('人生苦短,我用python')
king.run()