学校信息门户模拟登录之密码加密

以前写的爬虫无法登录到学校的信息门户上去了,因为门户的新 JS 代码将表单的密码先加密了一次,再将其与别的表单数据 POST 过去。使用的是 AES 加密的 CBC 模式。

本文前半部分是我的 python 组长雁横给组员们讲解的信息门户的密码加密思路,然后由我总结成文,后半部分是我自己写的加密代码实现,使用 python 的PyCryptodome库来进行加密。

参考链接


本文代码的 github 链接


问题描述

登录之后查看原本提交表单的部分可以发现,密码由明文传输改成密文传输了。于是原本只需要 POST 账号和密码的明文就行,现在需要多经过一步——加密。

起码咱们学校还是有考虑安全问题嘛!OVO

分析加密过程

因为登录到主页的时候已经是加密好的密码,所以加密工作应该是在登录页面就进行的。

所以回到登录页面刷新一下,筛选 javascript 文件(因为 js 文件是用于动作的)

在这几个 js 文件中找找有没有线索,然后在其中一个 js 文件中找到了一个密码加密函数。

encryptPassword()

传入密码,返回加密后的密码。

1
2
3
4
5
6
7
8
function encryptPassword(pwd0) {
try {
var pwd1 = encryptAES(pwd0, pwdDefaultEncryptSalt);
$("#casLoginForm").find("#passwordEncrypt").val(pwd1);
} catch (e) {
$("#casLoginForm").find("#passwordEncrypt").val(pwd0);
}
}

核心逻辑就一句。

1
var pwd1 = encryptAES(pwd0, pwdDefaultEncryptSalt);

encryptPassword()调用了一个名为encryptAES()的函数,参数pwd0可能是未加密的密码,pwdDefaultEncryptSalt可能是加密用的密钥。try-catch 不用说了,就是错误处理。

encrypt 是加密的意思,而 AES 是一种加密的方式。

而刚刚的 js 文件里面有一个文件就带着 encrypt 这个单词,点进去看,找到了下一个函数:

encryptAES()

传入密码明文和 AES 密钥,返回密文。

1
2
3
4
5
6
7
8
9
10
11
12
function encryptAES(data, aesKey) {
//加密
if (!aesKey) {
return data;
}
var encrypted = getAesString(
randomString(64) + data,
aesKey,
randomString(16)
); //密文
return encrypted;
}

代码逻辑:

  • 如果没有给出密钥,那么就不加密直接返回明文;
  • 如果给出了密钥,那么就调用getAesString()函数来获取密文
  • 返回密文

其中randomString()函数代码如下:

1
2
3
4
5
6
7
8
9
var $aes_chars = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
var aes_chars_len = $aes_chars.length;
function randomString(len) {
var retStr = "";
for (i = 0; i < len; i++) {
retStr += $aes_chars.charAt(Math.floor(Math.random() * aes_chars_len));
}
return retStr;
}

从上图可以看到,getAesString()就在这个函数上方。

getAesString()

传入明文、密钥、偏移量,返回密文。

1
2
3
4
5
6
7
8
9
10
11
12
13
//AES-128-CBC加密模式,key需要为16位,key和iv可以一样
function getAesString(data, key0, iv0) {
//加密
key0 = key0.replace(/(^\s+)|(\s+$)/g, ""); //去除开头和结尾的空白
var key = CryptoJS.enc.Utf8.parse(key0);
var iv = CryptoJS.enc.Utf8.parse(iv0);
var encrypted = CryptoJS.AES.encrypt(data, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
});
return encrypted.toString(); //返回的是base64格式的密文
}

在这个函数中调用了 aes 加密算法的函数来加密

密钥

还差密钥pwdDefaultEncryptSalt,去 js 文件里面搜索:

图中可以看到,密钥的来源是pwdEncryptArr[1]变量,但是在 js 文件里面却找不到这个从哪里来的了。

不过去搜索登录页面源代码的时候发现它就写在页面里面。

得到了密钥:

1
var pwdDefaultEncryptSalt = "QgkxfHXdbwRHcvDI";

后来发现,这个密钥同样每次都会变化,可以在获取表单变化的隐藏域值的时候顺便获取了。

总结

信息门户加密算法是:AES-128-CBC 加密模式,key 需要为 16 位,key 和 iv 可以一样(从注释里面得到的)

  • 密文 data 是长度为 64 的随机字符串与登录密码连接。
  • 密钥 key 就放在登录页面源码内,每次都会变化,需要动态获取。
  • 偏移量 iv 是长度为 16 的随机字符串。

现在知道了它的加密算法以及密钥,我们在模拟登录的时候把我们的密码用同样的方式加密,向以前那样发送就可以登录了。

有两种解决方案:

  1. 直接在 python 里面运行复制来的 js 代码。参考:Python 运行 js 代码
  2. 使用 python 进行加密

加密 python 代码实现

AES 简介

AES(Advanced Encryption Standard)(高级加密标准),用于代替原本的 DES(Data Encryption Standard)

2006 年,高级加密标准已然成为对称密钥加密中最流行的算法之一。——搜狗百科

AES 算法将明文分为长度相等的若干组,每次加密一组数据。

分组长度固定为 128 位(16 字节),密钥长度则可以是 128,192 或 256 比特(16、24 和 32 字节)。

我遇到的加密问题需要的是 128 位的密钥。

PyCryptodome 库

这个库是 PyCrypto 库(已经停止更新)的延续。

PyCryptodome 库的官方文档

安装方式(windows):

1
pip install pycryptodomex

导入方式:

1
import Cryptodome

获取密钥

在页面源码里面密钥的格式是:

1
var pwdDefaultEncryptSalt = "QgkxfHXdbwRHcvDI";

使用正则表达式来解析:

1
2
3
4
5
6
7
8
9
10
def get_encrypt_salt(login_url):
'''
获取密钥
:param login_url:登录页面的url
:return: (密钥,密钥对应的cookies)
'''
response=requests.get(login_url)
pattern = re.compile('var\s*?pwdDefaultEncryptSalt\s*?=\s*?"(.*?)"')
pwdDefaultEncryptSalt = pattern.findall(response.text)[0]
return (pwdDefaultEncryptSalt,response.cookies)

获取初始化向量

iv 是初始化向量,也称作偏移量。

在上面的分析中,传给加密函数的 iv 是一个随机字符串:

1
var encrypted = getAesString(randomString(64) + data, aesKey, randomString(16)); //密文

现在用 python 来实现这个randomString()

randomString()的 python 实现

1
2
3
4
5
6
7
8
9
10
def random_string(length):
'''
获取随机字符串
:param length:随机字符串长度
'''
ret_string=''
aes_chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678'
for i in range(length):
ret_string+=random.choice(aes_chars)
return ret_string

那一串用于随机的字符串是我从 js 文件的注释里面复制下来的,这个串并没有覆盖全部的字母和数字,为了防止意外,直接使用它的。

getAesString()的 python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad#用于对齐
import base64
def get_aes_string(data,key,iv):
'''
用AES-CBC方式加密字符串
:param data: 需要加密的字符串
:param key: 密钥
:param iv: 偏移量
:return: base64格式的加密字符串
'''
#预处理字符串
data=str.encode(data)
data=pad(data, AES.block_size)#将明文对齐

#预处理密钥和偏移量
key=str.encode(key)
iv=str.encode(iv)

cipher = AES.new(key, AES.MODE_CBC, iv)#初始化加密器
cipher_text=cipher.encrypt(data)#加密

#返回的是base64格式的密文
cipher_b64=str(base64.b64encode(cipher_text), encoding='utf-8')
return cipher_b64

对齐

首先,先将明文对齐,因为 AES 加密是分组加密,所以明文的长度需要是组长度的倍数。

有两种方式

  1. Cryptodome.Util.Padding中的 pad 函数就可以实现对齐,就是我采用的办法。
  2. 组长雁横的代码是这样实现对齐的:
1
2
3
4
def add_to_16(value):
while len(value) % 16 != 0:
value += '\0'
return str.encode(value) # 返回bytes

预处理

js 代码里面在加密之前,对数据做了编码处理:

1
2
var key = CryptoJS.enc.Utf8.parse(key0);
var iv = CryptoJS.enc.Utf8.parse(iv0);

因此也顺便处理一下。

python encode 方法

描述

Python encode() 方法以 encoding 指定的编码格式编码字符串。errors 参数可以指定不同的错误处理方案。

语法

encode()方法语法:

1
str.encode(encoding='UTF-8',errors='strict')

encryptAES()的 python 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def encrypt_aes(data,key=None):
'''
进行AES加密
:param data: 需要加密的字符串
:param key: 密钥
:return: 如果key存在,则返回密文,否则返回明文
'''
if(not key):
return data
else:
data=random_string(64)+data
iv=random_string(16)#偏移量
encrypted =get_aes_string(data,key,iv)
return encrypted

encryptPassword()的 python 实现

1
2
3
4
5
6
7
8
9
10
11
def encrypt_password(password,login_url):
'''
加密密码
:param password: 需要加密的密码
:param login_url:登录页面的url
:return: (加密后的密码,对应的cookies)
'''
key,cookies=get_encrypt_salt(login_url)
password.strip()#去除头尾空格
encrypted=encrypt_aes(password,key)
return (encrypted,cookies)

这就完成了

测试代码

1
2
3
4
5
6
if __name__ == '__main__':
login_url='http://ids.chd.edu.cn/authserver/login?service=http%3A%2F%2Fportal.chd.edu.cn%2F'

password=input('password:')
password,cookies=encrypt_password(password,login_url)
print('encrypted password:',password)

有效性检验

可以使用浏览器开发者工具的控制台,调用 js 函数,传入同样的参数,看是否得到相同的结果。

测试结果如图:


学校信息门户模拟登录之密码加密

https://yxchangingself.xyz/posts/3903089268/

作者

憧憬少

发布于

2019-07-18

更新于

2019-07-18

许可协议