使用Python模拟执行JavaScript
通过一些调试,我们发现加密参数token是由encrypt方法产生的。如果里面的逻辑相对简单的话,那么我们可以用Python完全重写一遍。但是现实情况往往不是这样的,一般来说,一些加密相关的方法通常会引用一些相关标准库,比如说JavaScript就有一个广泛使用的库,叫作crypto-js,这个库实现了很多主流的加密算法,包括对称加密、非对称加密、字符编码等。比如对于AES加密,通常我们需要输入待加密文本和加密密钥,实现如下:
const ciphertext = CryptoJS.AES.encrypt(message, key).toString();
对于这样的情况,我们其实没法很轻易地完全重写一遍,因为Python中并不一定有和JavaScript完全一样的类库。既然JavaScript已经实现好了,那么我用Python直接模拟执行这些JavaScript得到结果不就好了吗?
1.案例引入
案例网站链接 https://spa7.scrape.center/,如图所示:
这是一个NBA球星网站,用卡片的形式展示了一些球星的基本信息。另外,每张卡片上其实都有一个加密字符串,这个加密字符串其实和球星的信息是有关联的,并且每个球星的加密字符串也是不同的。所以,要做的就是找出这个加密字符串的加密算法并用程序把加密字符串的生成过程模拟出来。
2.准备工作
我们需要使用Python模拟执行JavaScript,这里使用的库叫PyExecJS。我们使用pip3命令安装它,如下:
pip3 install pyexecjs
PyExecJS是用于执行JavaScript的,但执行JavaScript的功能需要依赖JavaScript运行环境,所以除了安装好这个库外,还需要安装一个JavaScript运行环境,首选Node.js。更加详细的安装和配置过程可以参考https://setup.scrape.center/pyexecjs。
PyExecJS库在运行时会检测本地JavaScript运行环境来实现JavaScript执行,运行代码检查一下运行环境:
import execjs
print(execjs.get().name)
运行结果如下:
Node.js (V8)
证明环境运行正常。
3.分析
接下来,我们就对这个网站稍作分析。打开Sources面板,我们可以非常轻易地找到加密字符串的生成逻辑,如图所示:
首先,声明一个球员相关的列表,如:
const players = [
{
name: '凯文-杜兰特',
image: 'durant.png',
birthday: '1988-09-29',
height: '208cm',
weight: '108.9KG'
},
....
]
然后对于每一个球员,我们调用加密算法对其信息进行加密。我们可以添加断点看看,如图所示:
可以看到, getToken方法的输入就是单个球员的信息,就是上述列表的一个元素对象,然后this.key就是一个固定的字符串。整个加密逻辑就是提取球员的名字、生日、身高、体重、接着先进行Base64编码,然后进行DES加密,最后返回结果。
加密算法的实现就是依赖了crypto-js库,使用CryptoJS对象来实现的。这个网站就是直接引用了crypto-js库,如图所示:
执行crypto-js库对应的这个JavaScript文件之后,CryptoJS就被诸如浏览器全局环境下,因此我们就可以在别的方法里直接使用CryptoJS对象里的方法了。
4.模拟调用
首先,要模拟的其实就是这个getToken方法,输入球员相关的信息,得到最终的加密字符串。这里直接把key替换下,把getToken方法稍微改写一下,具体如下:
function getToken(player) {
let key = CryptoJS.enc.Utf8.parse("fipFfVsZsTda94hJNKJfLoaqyqMZFFimwLt")
const {
name, birthday, height, weight} = player;
let base64Name = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(name));
let encrypted = CryptoJS.DES.encrypt(`${
base64Name}${
birthday}${
height}${
weight}`, key,
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.toString();
}
因为这个方法的模拟执行需要CryptoJS这个对象,如果我们直接调用这个方法,肯定会报CryptoJS未定义的错误。所以只需要再模拟执行一下刚才看到的crypto-js.min.js就可以了。
模拟运行crypto-js.min.js里面的JavaScript,用于声明CryptoJS对象。
模拟运行getToken方法的定义,用于声明getToken方法。
把crypto-js.min.js里面的代码和上面getToken方法的代码复制一下,都粘贴到一个JavaScript文件里面,比如叫crypto.js。
接下来,就用PyExecJS模拟执行一下,代码如下:
import execjs
import json
item = {
'name': '凯文-杜兰特',
'image': 'durant.png',
'birthday': '1988-09-29',
'height': '208cm',
'weight': '108.9KG'
}
file = 'moni.js'
node = execjs.get()
ctx = node.compile(open(file).read())
js = f"getToken({json.dumps(item,ensure_ascii=False)})"
print(js)
result = ctx.eval(js)
print(result)
这里单独定义了一位球员的信息,将其赋为item变量。然后使用execjs的get方法获取JavaScript执行环境,赋值为node。
接着,调用node的compile方法,这里给它传入刚才定义的crypto.js文件的文本内容。compile方法会返回一个JavaScript的上下文对象,我们将其赋给ctx。执行到这里,其实就可以理解为,ctx对象里面执行了过了crypto-js.min.js,CryptoJS就声明好了,然后紧接着getToken方法的声明代码也被执行,所以getToken方法也定义好了,相当于完成了一些初始化工作。
只需要定义我们想要执行的JavaScript代码,定义一个js变量,其实就是模拟调用了getToken方法并传入了球员信息。打印js变量的值,内容如下:
getToken({
"name": "凯文-杜兰特", "image": "durant.png", "birthday": "1988-09-29", "height": "208cm", "weight": "108.9KG"})
其实这就是一个标准的JavaScript方法调用的写法而已。接着调用ctx对象的eval方法并传入js变量,其实就是模拟执行这句JavaScript代码,照理来说最终返回的就是加密字符串了。
然后,运行之后,我们可能看到这个报错:
execjs._exceptions.ProgramError: ReferenceError: CryptoJS is not defined
很奇怪,CryptoJS未定义?明明执行过crypto-js.min.js里面的内容了呀?
问题其实处在crypto-js.min.js,可以看到其中声明了一个JavaScript的自执行方法,如图所示。
什么是自执行方法呢?就是声明了一个方法,然后紧接着调用执行。可以看下这个例子:
!(function(a, b){
console.log('result', a, b)})(1, 2)
这里先声明了一个function,它接收a和b两个参数,然后把内容输出出来,接着我们把这个function用小括号括起来。这其实就是一个方法,可以被直接调用,怎么调用呢?后面再跟上对应的参数就好了,比如传入1和2,执行结果如下:
result 1 2
可以看到,这个自执行方法就被执行了。
同理,crypto-js.min.js也符合这个格式,它接收t和e两个参数,t就是this,其实就是浏览器中的window对象,e就是一个function(用于定义CryptoJS的核心内容)。
我们再来观察下crypto-js.min.js开头的定义:
"object" == typeof exports ? module.exports = exports = e() : "function" == typeof define && define([], e) : t.CryptoJS = e());
在Node.js中,其实exports用来将一些对象的定义导出,这里“object” == typeof exports的结果其实就是true,所以就执行了module.exports = exports = e()这段代码,这相当于把e()作为整体导出,而这个e()其实就对应后面的整个function。function里面定义了加密相关的各个实现,其实就指代整个加密算法库。
但是在浏览器中,结果就不一样了,浏览器中没有exports和define这两个对象。所以上述代码在浏览器中最后执行的就是t.CryptoJS = e()这段代码,其实这里就是把CryptoJS对象挂载到全局对象里面,所以后面我们再调用CryptoJS就自然出现未定义的错误了。
其实很简单解决这个问题,直接声明一个CryptoJS变量,然后手动声明一下它的初始化就好了,代码修改如下:
var CryptoJS; //声明变量
!function(t, e) {
CryptoJS = e(); //初始化
"object" == typeof exports ? module.exports = exports = e() : "function" == typeof define && define.amd ? define([], e) : t.CryptoJS = e()
}(this, function() {
//..
});
这里首先声明一个CryptoJS变量,然后给CryptoJS变量赋值e(),这样就完成了CryptoJS的初始化。这样再重新运行这个Python脚本,执行结果如下:
bruce_liu@localhost 模拟执行JavaScript % python3 pyexecjs.py
getToken({
"name": "凯文-杜兰特", "image": "durant.png", "birthday": "1988-09-29", "height": "208cm", "weight": "108.9KG"})
DG1uMMq1M7OeHhds71HlSMHOoI2tFpWCB4ApP00cVFqptmlFKjFu9RluHo2w3mUw
这样我们就成功得到加密字符串了,和示例网站上显示的一模一样,成功模拟JavaScript的调用完成了某个加密算法的运行过程。