通过上一篇文章之后相信大家对于web js逆向已经有了一个初步的认识,可能都认为逆向也不过如此嘛,如果有这种想法那就大错特错!要知道在上篇文章案例中的网站是使用md5加密算法来对参数进行加密,所以要改写成Python的话还是非常容易的,那如果别人网站使用的是自己实现的算法或者魔改后的常规算法呢?并且这还算好的,至少还能看,如果遇到混淆的话,那代码简直是不堪入目。所以,这种情况下,与其改写代码,不如直接让其算法在我们的本地运行,通过Python获取到本地运行的结果来实现参数的生成,接下来我们就正式进入本篇文章的内容。
一、环境配置
本地要想执行js代码的话,在本地就必须存在着执行js代码的环境,就像Python代码要想执行的话环境中就需要存在python解释器一样。本篇文章中以windows系统为例。
1.1 Node.js下载
下载地址:https://nodejs.org/en/download
此处直接下载的是18.16.0的版本,如果需要其他版本则在当前页面向下翻动后点击自行选择Previous Releases
1.2 node.js安装
下载完成之后,直接双击打开安装包。
看到欢迎界面后直接点击next。然后进入到协议页面选择接受然后点击next。如下图。
然后来到安装目录选择页面,在这里可以自定义安装目录,但是切忌,目录中最好不要存在中文字符包括一些特殊符号也不要存在。
安装目录选择之后一直点击next直到看到install下载按钮。
点击install,等待安装结束。安装结束后直接点击finish即可。
接着打开终端输入node -v、
npm -v`测试安装是否成功。
如此便证明安装是成功了。接下来便是在pycharm中配置js的执行环境了,当然,也可以使用文本然后更改扩展为js,例如下方输出hello world
的代码。
代码编辑完成之后另存为js文件。这里一定要注意文件扩展名不要写错。
然后在终端找到文件所在目录,使用node 文件名
命令执行。
但是呢很明显的可以感受到这样的话非常不方便,所以最好还是有相应的编辑器,直接使用jet全家桶之一的WebStorm或者用VSCode都可以,看自己喜欢使用什么,当然直接用pycharm也行,但是pycharm的话必须是专业版的pycharm,社区版是无法安装nodejs插件的,接下来我们就来看pycharm中配置js执行环境的过程。
1.3 pycharm配置js执行环境
首先打开pycharm设置
选择plugins,然后选中marketplace,再在搜索框输入nodejs
此处我的是已经下载好了,没有下载过的话是绿色的可点击样式,直接点击下载,在下载的过程中先去配置其他选项。
选择Languages & Frameworks然后选择Node.js
在右侧配置nodejs与其包管理工具npm的路径,当然pycharm版本较新并且nodejs的环境变量配置无误的话此处会自动识别,如果没有自动识别出来的话只需要手动配置即可,注意查看图中的路径。
到此,pycharm中的配置结束,等待插件下载安装结束之后重新打开pycharm便可以直接在pycharm中编写并执行js代码了。注意创建代码文件的时候是创建JavaScript文件,不要创建成py文件了。
二、pyexecjs
python要执行js代码的话那么必须要有python执行js的依赖,也就是说需要安装第三方包。而类似的第三方包其实有很多,比较常用的就有js2py
和pyexecjs
等,前者适用于js代码较少的情况,也就意味着如果遇到了很长的混淆代码的话,转译就可能出现报错,因此更多的还是选择pyexecjs进行使用。
2.1 安装
通过包管理工具进行安装:pip install pyexecjs
2.2 使用方法
内容较简单且为固定用法,不再截图展示。提供代码如下。
import execjs # 导入execjs模块
from execjs.runtime_names import Node # 导入NodeJS环境
ndjs = execjs.get(Node) # 修改为NodeJS执行环境,除了Nodejs之外还有其他的环境也能够执行js代码,自行了解即可
print(ndjs.name) # 输出结果为Node.js (V8)
# 方式1,使用eval方法直接执行
r1 = ndjs.eval("'learning net spider by quanmou!'.split(' ')")
print(r1) # ['learning', 'net', 'spider', 'by', 'quanmou!']
# 方式2,先加载再执行
# 同级目录下新建一个js文件,名为prac.js,内容为function add(a, b) {
# return a+b
# }
jscode = open('prac.js', encoding='utf-8').read() # 读取出js文件中的js代码
js_compile = ndjs.compile(jscode) # 加载器先加载js代码,注意此时代码并没有执行
a = 1
b = 2
r2 = js_compile.call('add', a, b) # 通过加载器call方法调用已加载的js代码中的函数,在call方法中第一个参数为要调用的函数的函数名,
# 后面的参数为调用函数是需要传递的参数,注意参数要一一对应
print(r2) # 输出3
三、百度翻译案例
接下来我们以百度翻译为例进行简单实战。老规矩进入到百度翻译页面后直接打开开发者工具进行分析。
毫无疑问是异步加载的,所以直接定位到XHR进行抓包。在输入框输入需要翻译的内容就能抓到翻译时请求的接口。
定位到包后打开查看详情,直接预览,在trans_result节点中明显看到了翻译的结果,所以直接就从这个包入手就OK。
到这里,数据包已经找到了,那么接下来只要去请求这个包所在的URL就能够拿到对应的响应,其中就会包含我们需要的翻译结果。
那么现在来实现第一部分的代码。
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36',
'Acs-Token': '替换为自己的token',
'Cookie': '替换为自己的Cookie'
}
url = 'https://fanyi.baidu.com/v2transapi?from=zh&to=en'
data = {
"from": "zh",
"to": "en",
"query": "美女", # 需要翻译的内容
"transtype": "realtime",
"simple_means_flag": "3",
"sign": "551517.821612",
"token": "替换为自己的token",
"domain": "common"
} # 构建表单数据
response = requests.post(url, headers=headers, data=data)
print(response.json())
注意:该接口存在token认证机制以及cookie认证,所以代码中需要在请求头添加token和cookie参数值。在浏览器中查看翻译接口返回的数据包的详情信息中,在请求头里面直接复制即可。
此时输出结果如下
但是,当我们修改表单中query的值后,便会发现无法获取到翻译结果了。
那么具体原因是什么呢?回到浏览器,我们翻译帅哥然后抓包看一下有什么不同的地方呢?
对比美女传输的表单数据来看的话,翻译帅哥传书的表单数据的sign值是不同于美女的。
那是是否就是这个值影响了我们翻译的结果呢,直接来测试一下,将代码中的sign值修改为此处看到的sign值。然后执行代码看结果。
的确,结果已经出来了,至此,我们就知道了在token与cookie不变的情况下,由query和sign控制我们要翻译的内容与结果,所以接下来只需要找到sign参数的生成位置即可。sign是表单数据,所以可以通过搜索源代码的方式来定位,快捷键为:ctrl+shift+f。
为了避免出现过多的无效搜索结果,我们可以根据参数的格式添加特殊符号,以减少搜索到的无效结果的数量,例如此处我们就可以添加冒号来实现。
搜索到三个包,挨个进行查看。此处第一个就是目标。点击即可进入到源代码之中。
进入之后在该js文件中继续搜索sign
de 位置
可以看到一共有7个结果,不熟练的情况下我们就挨个打上断点然后重新抓包,当代码被断下则证明断点处于我们的sign有关。
定位到第四个sign时重新翻译的时候代码执行在此处停下,那么也就意味着此处就是我们需要的sign的内容。很明显的可以看到,sign调用b方法返回的结果,传入的参数e就是我们要翻译的内容,那么接下来我们进入到b方法看一下这个参数是如何生成的呢。
定位b方法过来之后,我们可以知道b方法就是图中箭头所指的函数,阅读这个函数的代码可以发现,改写这个代码的话难度还是有的,所以完全没有必要对其进行改写,直接将这整个函数复制到本地的js文件中执行。函数名不好看的话我们可以稍微改写一下这个函数的格式。如下:
function inno(t) {
var o, i = t.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
if (null === i) {
var a = t.length;
a > 30 && (t = "".concat(t.substr(0, 10)).concat(t.substr(Math.floor(a / 2) - 5, 10)).concat(t.substr(-10, 10)))
} else {
for (var s = t.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), c = 0, l = s.length, u = []; c < l; c++)
"" !== s[c] && u.push.apply(u, function (t) {
if (Array.isArray(t))
return e(t)
}(o = s[c].split("")) || function (t) {
if ("undefined" != typeof Symbol && null != t[Symbol.iterator] || null != t["@@iterator"])
return Array.from(t)
}(o) || function (t, n) {
if (t) {
if ("string" == typeof t)
return e(t, n);
var r = Object.prototype.toString.call(t).slice(8, -1);
return "Object" === r && t.constructor && (r = t.constructor.name),
"Map" === r || "Set" === r ? Array.from(t) : "Arguments" === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) ? e(t, n) : void 0
}
}(o) || function () {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")
}()),
c !== l - 1 && u.push(i[c]);
var p = u.length;
p > 30 && (t = u.slice(0, 10).join("") + u.slice(Math.floor(p / 2) - 5, Math.floor(p / 2) + 5).join("") + u.slice(-10).join(""))
}
for (var d = "".concat(String.fromCharCode(103)).concat(String.fromCharCode(116)).concat(String.fromCharCode(107)), h = (null !== r ? r : (r = window[d] || "") || "").split("."), f = Number(h[0]) || 0, m = Number(h[1]) || 0, g = [], y = 0, v = 0; v < t.length; v++) {
var _ = t.charCodeAt(v);
_ < 128 ? g[y++] = _ : (_ < 2048 ? g[y++] = _ >> 6 | 192 : (55296 == (64512 & _) && v + 1 < t.length && 56320 == (64512 & t.charCodeAt(v + 1)) ? (_ = 65536 + ((1023 & _) << 10) + (1023 & t.charCodeAt(++v)),
g[y++] = _ >> 18 | 240,
g[y++] = _ >> 12 & 63 | 128) : g[y++] = _ >> 12 | 224,
g[y++] = _ >> 6 & 63 | 128),
g[y++] = 63 & _ | 128)
}
for (var b = f, w = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(97)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(54)), k = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(51)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(98)) + "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(102)), x = 0; x < g.length; x++)
b = n(b += g[x], w);
return b = n(b, k),
(b ^= m) < 0 && (b = 2147483648 + (2147483647 & b)),
"".concat((b %= 1e6).toString(), ".").concat(b ^ f)
}
将函数命名为inno,接下来就是直接调用inno函数传入翻译内容然后执行代码查看结果。
报错r没有定义,那么就近一步去查找r的位置,结合代码中报的未定义的r的位置来到浏览器中对应位置打上断点
然后执行代码到下一个断点,便可以看到代码断在了此处,那么此时就可以查看r的具体情况。
将鼠标放在r上可以看到r的具体值如上所示,同时,在函数邻近上方可以看到定义的r此时的值也为上图所示情况。
而对于r有关的赋值的话仅存在一个r=window[d]
,所以能够直接猜测出r的值来自于window对象,那么我们就来看一下这个值是如何生成的。
将页面左右滚动直到r=window[d]
打上断点然后执行点击执行到下一个断点处
此时将鼠标放在d上,可以看到d的值是一个字符串gtk
,那么至此我们就知道了,r的值就是window对象的gtk节点对应的值或者方法调用的返回结果,具体什么情况我们再进一步观察。
在console(控制台)输入window然后回车查看window对象,找到gtk节点对应的位置。
很明显了,是一个固定值,那么也就是说r其实也是一个固定值,所以回到我们扣下来的js代码中,定义一个r并且赋值即可。执行代码如下:
可以看到此时已经没有报r没有定义了,但是报出了n没有定义,n很明显是一个方法,那么我们以同样的方式去找到这个n方法的位置将整个n方法扣下来放到代码中运行即可。
将n函数粘贴到本地js后执行结果如下:
至此,js代码就全部搞定,该结果与浏览器中翻译美女时的sign也完全相同。
那么接下来只需要在python中调用js中的inno函数就可以获取拿到这个参数然后携带着去进行请求了。
代码如下:
from execjs.runtime_names import Node
...
ndjs = execjs.get(Node)
jscode = open('baidutrans.js', encoding='utf-8').read()
word = input('请输入您要翻译的内容:')
sign = ndjs.compile(jscode).call('inno', word)
data = {
"from": "zh",
"to": "en",
"query": word, # 需要翻译的内容
"transtype": "realtime",
"simple_means_flag": "3",
"sign": sign,
"token": "替换为自己的token",
"domain": "common"
} # 构建表单数据
response = requests.post(url, headers=headers, data=data) # 注意请求方式是post请求,所以不能使用get方法进行请求发送,data为表单数据
print(response.json())
执行结果:
至此,本案例告一段落,同时对于pyexecjs也有了初步的认知。后面为完整的py代码与js代码。
四、完整代码
4.1 完整JS代码
function n(t, e) {
for (var n = 0; n < e.length - 2; n += 3) {
var r = e.charAt(n + 2);
r = "a" <= r ? r.charCodeAt(0) - 87 : Number(r),
r = "+" === e.charAt(n + 1) ? t >>> r : t << r,
t = "+" === e.charAt(n) ? t + r & 4294967295 : t ^ r
}
return t
}
var r = '320305.131321201'
function inno(t) {
var o, i = t.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g);
if (null === i) {
var a = t.length;
a > 30 && (t = "".concat(t.substr(0, 10)).concat(t.substr(Math.floor(a / 2) - 5, 10)).concat(t.substr(-10, 10)))
} else {
for (var s = t.split(/[\uD800-\uDBFF][\uDC00-\uDFFF]/), c = 0, l = s.length, u = []; c < l; c++)
"" !== s[c] && u.push.apply(u, function (t) {
if (Array.isArray(t))
return e(t)
}(o = s[c].split("")) || function (t) {
if ("undefined" != typeof Symbol && null != t[Symbol.iterator] || null != t["@@iterator"])
return Array.from(t)
}(o) || function (t, n) {
if (t) {
if ("string" == typeof t)
return e(t, n);
var r = Object.prototype.toString.call(t).slice(8, -1);
return "Object" === r && t.constructor && (r = t.constructor.name),
"Map" === r || "Set" === r ? Array.from(t) : "Arguments" === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) ? e(t, n) : void 0
}
}(o) || function () {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")
}()),
c !== l - 1 && u.push(i[c]);
var p = u.length;
p > 30 && (t = u.slice(0, 10).join("") + u.slice(Math.floor(p / 2) - 5, Math.floor(p / 2) + 5).join("") + u.slice(-10).join(""))
}
for (var d = "".concat(String.fromCharCode(103)).concat(String.fromCharCode(116)).concat(String.fromCharCode(107)), h = (null !== r ? r : (r = window[d] || "") || "").split("."), f = Number(h[0]) || 0, m = Number(h[1]) || 0, g = [], y = 0, v = 0; v < t.length; v++) {
var _ = t.charCodeAt(v);
_ < 128 ? g[y++] = _ : (_ < 2048 ? g[y++] = _ >> 6 | 192 : (55296 == (64512 & _) && v + 1 < t.length && 56320 == (64512 & t.charCodeAt(v + 1)) ? (_ = 65536 + ((1023 & _) << 10) + (1023 & t.charCodeAt(++v)),
g[y++] = _ >> 18 | 240,
g[y++] = _ >> 12 & 63 | 128) : g[y++] = _ >> 12 | 224,
g[y++] = _ >> 6 & 63 | 128),
g[y++] = 63 & _ | 128)
}
for (var b = f, w = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(97)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(54)), k = "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(51)) + "".concat(String.fromCharCode(94)).concat(String.fromCharCode(43)).concat(String.fromCharCode(98)) + "".concat(String.fromCharCode(43)).concat(String.fromCharCode(45)).concat(String.fromCharCode(102)), x = 0; x < g.length; x++)
b = n(b += g[x], w);
return b = n(b, k),
(b ^= m) < 0 && (b = 2147483648 + (2147483647 & b)),
"".concat((b %= 1e6).toString(), ".").concat(b ^ f)
}
console.log(inno('美女'))
4.2 完整python代码
import requests
import execjs
from execjs.runtime_names import Node
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36',
'Acs-Token': '替换为自己的token',
'Cookie': '替换为自己的Cookie'
}
url = 'https://fanyi.baidu.com/v2transapi?from=zh&to=en'
ndjs = execjs.get(Node)
jscode = open('baidutrans.js', encoding='utf-8').read()
word = input('请输入您要翻译的内容:')
sign = ndjs.compile(jscode).call('inno', word)
data = {
"from": "zh",
"to": "en",
"query": word, # 需要翻译的内容
"transtype": "realtime",
"simple_means_flag": "3",
"sign": sign,
"token": "替换为自己的token",
"domain": "common"
} # 构建表单数据
response = requests.post(url, headers=headers, data=data) # 注意请求方式是post请求,所以不能使用get方法进行请求发送,data为表单数据
print(response.json())