学校信息门户模拟登录之密码加密
以前写的爬虫无法登录到学校的信息门户上去了,因为门户的新 JS 代码将表单的密码先加密了一次,再将其与别的表单数据 POST 过去。使用的是 AES 加密的 CBC 模式。
本文前半部分是我的 python 组长雁横给组员们讲解的信息门户的密码加密思路,然后由我总结成文,后半部分是我自己写的加密代码实现,使用 python 的PyCryptodome
库来进行加密。
参考链接
- 浏览器开发者工具基本使用教程-博客园
- Python 运行 js 代码
- python 加密与解密(大致介绍了加密解密算法)-博客园
- 常见加密方式与 python 实现-简书
- PyCryptodome 库的官方文档
- python encode 方法-菜鸟教程
问题描述
登录之后查看原本提交表单的部分可以发现,密码由明文传输改成密文传输了。于是原本只需要 POST 账号和密码的明文就行,现在需要多经过一步——加密。
起码咱们学校还是有考虑安全问题嘛!OVO
分析加密过程
因为登录到主页的时候已经是加密好的密码,所以加密工作应该是在登录页面就进行的。
所以回到登录页面刷新一下,筛选 javascript 文件(因为 js 文件是用于动作的)
在这几个 js 文件中找找有没有线索,然后在其中一个 js 文件中找到了一个密码加密函数。
encryptPassword()
传入密码,返回加密后的密码。
1 | function encryptPassword(pwd0) { |
核心逻辑就一句。
1 | var pwd1 = encryptAES(pwd0, pwdDefaultEncryptSalt); |
encryptPassword()
调用了一个名为encryptAES()
的函数,参数pwd0
可能是未加密的密码,pwdDefaultEncryptSalt
可能是加密用的密钥。try-catch 不用说了,就是错误处理。
encrypt 是加密的意思,而 AES 是一种加密的方式。
而刚刚的 js 文件里面有一个文件就带着 encrypt 这个单词,点进去看,找到了下一个函数:
encryptAES()
传入密码明文和 AES 密钥,返回密文。
1 | function encryptAES(data, aesKey) { |
代码逻辑:
- 如果没有给出密钥,那么就不加密直接返回明文;
- 如果给出了密钥,那么就调用
getAesString()
函数来获取密文 - 返回密文
其中randomString()
函数代码如下:
1 | var $aes_chars = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678"; |
从上图可以看到,getAesString()
就在这个函数上方。
getAesString()
传入明文、密钥、偏移量,返回密文。
1 | //AES-128-CBC加密模式,key需要为16位,key和iv可以一样 |
在这个函数中调用了 aes 加密算法的函数来加密
密钥
还差密钥pwdDefaultEncryptSalt
,去 js 文件里面搜索:
图中可以看到,密钥的来源是pwdEncryptArr[1]
变量,但是在 js 文件里面却找不到这个从哪里来的了。
不过去搜索登录页面源代码的时候发现它就写在页面里面。
得到了密钥:
1 | var pwdDefaultEncryptSalt = "QgkxfHXdbwRHcvDI"; |
后来发现,这个密钥同样每次都会变化,可以在获取表单变化的隐藏域值的时候顺便获取了。
总结
信息门户加密算法是:AES-128-CBC 加密模式,key 需要为 16 位,key 和 iv 可以一样(从注释里面得到的)
- 密文 data 是长度为 64 的随机字符串与登录密码连接。
- 密钥 key 就放在登录页面源码内,每次都会变化,需要动态获取。
- 偏移量 iv 是长度为 16 的随机字符串。
现在知道了它的加密算法以及密钥,我们在模拟登录的时候把我们的密码用同样的方式加密,向以前那样发送就可以登录了。
有两种解决方案:
- 直接在 python 里面运行复制来的 js 代码。参考:Python 运行 js 代码
- 使用 python 进行加密
加密 python 代码实现
AES 简介
AES(Advanced Encryption Standard)(高级加密标准),用于代替原本的 DES(Data Encryption Standard)
2006 年,高级加密标准已然成为对称密钥加密中最流行的算法之一。——搜狗百科
AES 算法将明文分为长度相等的若干组,每次加密一组数据。
分组长度固定为 128 位(16 字节),密钥长度则可以是 128,192 或 256 比特(16、24 和 32 字节)。
我遇到的加密问题需要的是 128 位的密钥。
PyCryptodome 库
这个库是 PyCrypto 库(已经停止更新)的延续。
安装方式(windows):
1 | pip install pycryptodomex |
导入方式:
1 | import Cryptodome |
获取密钥
在页面源码里面密钥的格式是:
1 | var pwdDefaultEncryptSalt = "QgkxfHXdbwRHcvDI"; |
使用正则表达式来解析:
1 | def get_encrypt_salt(login_url): |
获取初始化向量
iv 是初始化向量,也称作偏移量。
在上面的分析中,传给加密函数的 iv 是一个随机字符串:
1 | var encrypted = getAesString(randomString(64) + data, aesKey, randomString(16)); //密文 |
现在用 python 来实现这个randomString()
randomString()的 python 实现
1 | def random_string(length): |
那一串用于随机的字符串是我从 js 文件的注释里面复制下来的,这个串并没有覆盖全部的字母和数字,为了防止意外,直接使用它的。
getAesString()的 python 实现
1 | from Cryptodome.Cipher import AES |
对齐
首先,先将明文对齐,因为 AES 加密是分组加密,所以明文的长度需要是组长度的倍数。
有两种方式
Cryptodome.Util.Padding
中的 pad 函数就可以实现对齐,就是我采用的办法。- 组长雁横的代码是这样实现对齐的:
1 | def add_to_16(value): |
预处理
js 代码里面在加密之前,对数据做了编码处理:
1 | var key = CryptoJS.enc.Utf8.parse(key0); |
因此也顺便处理一下。
描述
Python encode() 方法以 encoding 指定的编码格式编码字符串。errors 参数可以指定不同的错误处理方案。
语法
encode()方法语法:
1 str.encode(encoding='UTF-8',errors='strict')
encryptAES()的 python 实现
1 | def encrypt_aes(data,key=None): |
encryptPassword()的 python 实现
1 | def encrypt_password(password,login_url): |
这就完成了
测试代码
1 | if __name__ == '__main__': |
有效性检验
可以使用浏览器开发者工具的控制台,调用 js 函数,传入同样的参数,看是否得到相同的结果。
测试结果如图:
学校信息门户模拟登录之密码加密