实现基于python的Web联网认证自动登录脚本
前言研一这几个月,每天最头疼的事莫过于联网还得登录。作为一个懒人,如此的重复劳动属实是不能接受的。所以,为了解决这个问题,web联网认证自动脚本,它来了。????本文代码地址:https://github.com/imcyx/SIAS-AutoLink可执行文件:https://github.com/imcyx/SIAS-AutoLink/releases/tag/V1.0.0如果无需了解原理,可
前言
研一这几个月,每天最头疼的事莫过于联网还得登录。作为一个懒人,如此的重复劳动属实是不能接受的。所以,为了解决这个问题,web联网认证自动脚本,它来了。😍
本文代码地址:https://github.com/imcyx/SIAS-AutoLink
可执行文件:https://github.com/imcyx/SIAS-AutoLink/releases/tag/V1.0.0
如果无需了解原理,可以直接跳到第三章PC部署阅读。
目录
文章目录
一、报文解析
1. 抓包
实现web联网认证的自动执行脚本,第一步就是得弄明白报文的地址和格式,然后只需要让代码替代请求即可完成。作为一个写了这么多爬虫的老“爬手”,So easy 是不是?
显然,我想多了😢。我们先退出登录,然后打开:登录网关ip。打开的登陆界面如图所示:
Tip: 这里网关地址原本是(http://1.1.1.3),不知道为啥突然变了。
图1. 登录界面
这里我们按下Ctrl+Shift+I,打开Chrome浏览器的网络解析器,勾选Preverse log选项。然后在登录框输入账号密码,选择登录,对登录请求进行抓包并分析。最终抓包结果如图所示:
图2. 登录网络请求解析
可以看到,我们按下登录按钮后,网络请求总共涉及图2左边列出的资源。其中第一个login.php就是我们的登录请求,因为在请求完成后,页面html和其它资源才开始下载。
我们点击它,详细信息在右边显示,包括请求头、返回头、payload、cookies等信息。我们最关注的是payload里提交的信息,这里应该就是我们自己的账号密码等信息,点击查看结果如下:
图3. payload信息
首先一眼看到,这里payload的表单信息包括登录方式、登陆账号、登录密码、认证tag以及记住密码选项。
但,仔细研究发现事情并没有那么简单。这个pwd里的信息是我输入的密码?介不扯淡嘛~我的密码可是6位,这可是12位!
还有,这个tag是什么玩意?这么一长串是啥?…………
等等,仔细看看有点眼熟啊,该不会是————时间戳?再重登录一次看看:
图4. payload信息2
好嘛,就最后几位变了,前面都没变,看来这个auth_tag就是时间戳。但和我之前看的不一样,这玩意应该是一个从1970.1.1至今的毫秒时间戳,因为比以前的时间戳多了三位。
而且可以看到,pwd又全部都变化了。这说明我们之前太native了,这个登录请求并没有那么简单。在我们输入登陆的同时,它对我们的输入密码进行加密了。而且,因为上传时包括了时间戳,很有可能就是基于时间戳的加密算法。但究竟是怎么加密的呢?我们并不是很清楚。
欸,看来很难搞啊,难道这就gg了?难道又要祭出selenium了?
不能轻言放弃啊,我决定再研究一下。根据抓包的结果,从登陆界面开始到登陆成功为止,这段时间的请求里login.php很明显是最早开始的,而它的请求里密码已经加密了。在这之前我压根就没有输入密码,而且就算是实时加密的,那不管是抓包结果还是实际经济性程度那都是不合理的。所以,结果就只有一个,密码是在前端直接加密了!是在我按下登陆的同时前端实时加密然后再上传的。
那这不就好办了😏,前端 JS 代码里肯定能找加密代码。
2. 查询JS源码
说干就干,我们打开login.php的Initiator,查看他的call stack:
图5. Initiator信息
前两个不用说,jquery里应该是没有的。3~5使用的是logic_new.js,在这个文件完成了请求的包装,看来,八九不离十就在这里面。其中第三句是ajax实现,第四句是登录请求实现,第五句是密码登录包装。毋庸置疑,第五句,查!
我们打开Sources栏,查看前端处理的文件,按照地址找到logic_new.js。
注意,这里我们现在是在登陆成功之后的页面里,查找当前页面的资源是找不到这些资源的。我们得退出登录再到登录页面查看这些资源。
图6. 查看登陆页面前端logic_new.js
进来看到这个js文件的功能应该就是主要在前端进行登录报文处理,而且可以看出这应该是个类似于Baseline的文件(我也不知道这里咋叫,总之就类似于个demo),是一个能提供给前端开发扩展的处理库。里面又一大堆登录demo,我们学校的系统只用到了很小的一部分(密码登录)。
我们定位之前锁定的line 640,看一下onPwdLogin函数的主要处理逻辑:
图7. onPwdLogin函数代码段
虽然我对JS基本一窍不通,但这段代码凭借多年经验还是能看个大概的。在前面检查完成后,line 625获得rckey时间戳,line 626基于时间戳和用户键入的密码调用do_encrypt_rc4函数进行加密,返回加密后的密码pwd。line 627-639都在对请求参数进行合成,并在line 640调用loginRequest函数进行请求。
所以,归根结底,加密算法来自do_encrypt_rc4函数。我们定位函数:
图8. do_encrypt_rc4函数代码段
果然,历经千山万水,找到了你呀~
这个函数才是最终加密的地方,用户输入的原始密码和毫秒时间戳分别作为src和passwd参数输入函数并最终处理返回一串加密字符串。而这里使用的算法是大名鼎鼎的RC4加密算法(好吧,虽然我没用过但听说挺有名的:happy:)。
3. RC4加密及实现原理
参考这篇介绍RC4加密算法比较清楚的文章:RC4加密算法原理简单理解,简单分析一下这里的加密原理:
图9. RC4加密算法流程图
初始化密钥key(这里是由时间戳生成的,长度任意,用来作为密钥流生成的种子2)line 2434
输入长度小于256个字节,进行轮转填满
例如输入密钥的是1,2,3,4,5 , 那么填入的是1,2,3,4,5,1,2,3,4,5,1,2,3,4,5…(这里转换成十六进制表示)
for (i = 0; i < 256; i++){ key[i] = passwd.charCodeAt(i % plen); }
初始化状态向量sbox(256个字节,用来作为密钥流生成的种子1)line 2435
按照升序,给每个字节赋值0,1,2,3,4,5,6…,254,255
for (i = 0; i < 256; i++){ sbox[i] = i; }
对状态向量sbox进行置换操作(用来打乱初始种子1)line 2437-2442
j = 0; for (i = 0; i < 256; i++) { j = (j + sbox[i] + key[i]) % 256; // 交换sbox[i]和sbox[j] temp = sbox[i]; sbox[i] = sbox[j]; sbox[j] = temp; }
在该过程中,密钥key的主要功能是将S-box搅乱。而不同的S-box在经过伪随机子密码(这里就是时间戳)生成算法的处理后可以得到不同的子密钥序列,并且,该序列是随机的。
秘钥流的生成与加密
for (i = 0; i < size; i++) { a = (a + 1) % 256; b = (b + sbox[a]) % 256; temp = sbox[a]; sbox[a] = sbox[b]; sbox[b] = temp; c = (sbox[a] + sbox[b]) % 256; temp = src.charCodeAt(i) ^ sbox[c];//String.fromCharCode(src.charCodeAt(i) ^ sbox[c]); temp = temp.toString(16); if (temp.length === 1) { temp = '0' + temp; } else if (temp.length === 0) { temp = '00'; } output[i] = temp; }
这里前几句通过对状态向量转置并计算后得到子密码索引c和子密码sbox[c]。
然后,再利用子密码sbox[c]与明文字节进行XOR异或运算得到密文字节。
最终,对异或得到的密文字节转换成hex并格式对齐。再循环输出,合并即可得到完整密文。
这里最神奇的地方就是,利用时间戳作为密钥,原始密码经过加密得到密文。二者传送到后端后,再利用一模一样的代码根据密钥生成状态向量并异或密文,可以再次得到明文!!
而且RC4算法这么多年依然非常安全,除了弱口令密码存在子密钥序列重复进而被破解的可能性,但整体上算法加密效果依然非常好,非常的安全。且加密过程非常简单,适合简单的场景。
至此,我们摸透了前端登陆的流程,下面就是编写脚本实现了。
二、脚本编写
1. RC4算法的python实现
RC4加密过程是脚本实现的核心。按照上面所说的步骤,RC4算法的python实现如下:
# RC4加密算法
def do_encrypt_rc4(src:str, passwd:str)->str:
i, j, a, b, c = 0, 0, 0, 0, 0
key, sbox = [], []
plen = len(passwd)
size = len(src)
output = ""
# 初始化密钥key和状态向量sbox
for i in range(256):
key.append(ord(passwd[i % plen]))
sbox.append(i)
# 状态向量打乱
for i in range(256):
j = (j + sbox[i] + key[i]) % 256
temp = sbox[i]
sbox[i] = sbox[j]
sbox[j] = temp
# 秘钥流的生成与加密
for i in range(size):
# 子密钥生成
a = (a + 1) % 256
b = (b + sbox[a]) % 256
temp = sbox[a]
sbox[a] = sbox[b]
sbox[b] = temp
c = (sbox[a] + sbox[b]) % 256
# 明文字节由子密钥异或加密
temp = ord(src[i]) ^ sbox[c]
# 密文字节转换成hex,格式对齐修正(取最后两位,若为一位([0x0,0xF]),则改成[00, 0F])
temp = str(hex(temp))[-2:]
temp = temp.replace('x', '0')
# 输出
output += temp
return output
2. 模拟登录请求
实现了RC4算法,只需要模拟浏览器实现自动请求就可以了。这一部分的代码如下:
account = ""
password = ""
# 请求网址
url = "http://2.2.2.3/ac_portal/login.php"
# 请求头
headers = {
"X-Requested-With": "XMLHttpRequest",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", # 必须指定,否则报404
}
# 时间戳(提取ms单位)
tag = int(time.time()*1000)
# 利用RC4加密算法获取基于时间戳的密码
pwd = do_encrypt_rc4(password, str(tag))
# 账号、密码、时间戳写入payload报文
payload = f"opr=pwdLogin&userName={account}&pwd={pwd}&auth_tag={tag}&rememberPwd=1"
# 提交登录
res = requests.post(url, data=payload, headers=headers)
首先我们写入需要登陆的账号密码,并且指定请求网址和请求头。
注意,这里请求头里"X-Requested-With"和"User-Agent"为非必要指定,只是为了装配请求更加合规,"Content-Type"必须指定,否则后台无法解析请求会回报404错误。
然后我们生成当前时间戳,基于输入密码和时间戳加密获得密文,并将账号、密文、时间戳三者组合payload字符串,并作为data对服务器提交post请求。
我们可以打印返回内容,如果返回200则说明登陆成功,否则意味着账号密码错误。如果raise错误则意味着有可能没有链接wifi就执行了程序。成功返回结果如下:
{'success':true, 'msg':'logon success','action':'logout','pop':0,'userName':'183********','location':'http://2.2.2.3/ac_portal/proxy.html?type=logout'}
3. 异常处理
针对可能出现的异常,我们需要判断并进行处理。需要提示的信息还需要与用户进行交互,下面是改写后的异常处理及交互实现:
while True:
try:
# 提交登录
res = requests.post(url, data=payload, headers=headers)
# 输出登录结果
if res.status_code == 200 and res.text.find("true")>0:
print(f"\033[7;32;47m{res.content.decode('utf-8')} \033[0m")
else:
print(f"\033[7;31;47m{res.content.decode('utf-8')} \033[0m")
print("\033[7;31;47m", "Login fail! Make sure input true account info!", "\033[0m")
os.remove(file_name)
print("\033[7;36;47m", "Already clear account file.\n Restart EXE and input again.", "\033[0m")
print("Press any key to exit...")
# 等待退出
if ord(msvcrt.getch()):
break
# 如果请求出错,大概率网络未连
except Exception as err:
# 提示
print("\033[7;31;47m","Login Error!\tMaybe you need link wifi first?" ,"\033[0m")
# 输出error
print("\033[7;33;40m", err,"\033[0m")
# 按“R”再次执行脚本,按其它按键退出
print("Press 'R' to restart, or break...")
if ord(msvcrt.getch()) != 114:
break
如果登录正常,会以绿色反白输出请求返回内容,然后等待按键退出。
如果登录请求实现但无法登录,会以红色反白输出请求返回内容,提示登录错误并等待按键退出。
如果请求抛错,会提示链接WiFi并打印错误,然后摁下“R”可以继续运行脚本,其他键退出。
4. 账号密码输入
前面说完了基本脚本内容。为了实现更强的适用性,针对更广泛的部署,我们将账号密码由交互形式让用户提供,并以json形式保存在用户本地,不固化在程序里。代码段如下:
# 切换工作目录到系统临时文件区域
os.chdir(os.getenv('TMP'))
# 文件名称
file_name = "user_account.json"
# 如果不存在临时文件,根据输入新建临时文件保存账号密码
if not os.path.exists(file_name):
# 输入账号密码
account = input("Please input your account: ")
password = input("Please input your password: ")
res = {
'account': account,
'password': password
}
print(res)
# 写入json
with open(file_name, "w+") as fp:
fp.write(json.dumps(res))
# 如果存在,说明之前已经写入
else:
# 读取账号密码
with open(file_name, "r")as fp:
res = json.loads(fp.read())
account = res['account']
password = res['password']
这样,用户在第一次部署后就无需再次输入账号密码。如果账号密码有误,第一次登录就会出错,此时程序会自动删除错误账号密码保存的json并要求重新输入。
5. 完整代码
# -*- coding: utf-8 -*-
# @Time : 2022/1/13 22:01
# @Author : CYX
# @Email : im.cyx@foxmail.com
# @File : login_network.py
# @Software: PyCharm
import msvcrt
import requests
import time
import json
import os
# 使cmd能够正确输出颜色
if os.name == "nt":
os.system("")
# 切换工作目录到系统临时文件区域
os.chdir(os.getenv('TMP'))
# 文件名称
file_name = "user_account.json"
# 如果不存在临时文件,根据输入新建临时文件保存账号密码
if not os.path.exists(file_name):
# 输入账号密码
account = input("Please input your account: ")
password = input("Please input your password: ")
res = {
'account': account,
'password': password
}
print(res)
# 写入json
with open(file_name, "w+") as fp:
fp.write(json.dumps(res))
# 如果存在,说明之前已经写入
else:
# 读取账号密码
with open(file_name, "r")as fp:
res = json.loads(fp.read())
account = res['account']
password = res['password']
# RC4加密算法
def do_encrypt_rc4(src:str, passwd:str)->str:
i, j, a, b, c = 0, 0, 0, 0, 0
key, sbox = [], []
plen = len(passwd)
size = len(src)
output = ""
# 初始化密钥key和状态向量sbox
for i in range(256):
key.append(ord(passwd[i % plen]))
sbox.append(i)
# 状态向量打乱
for i in range(256):
j = (j + sbox[i] + key[i]) % 256
temp = sbox[i]
sbox[i] = sbox[j]
sbox[j] = temp
# 秘钥流的生成与加密
for i in range(size):
# 子密钥生成
a = (a + 1) % 256
b = (b + sbox[a]) % 256
temp = sbox[a]
sbox[a] = sbox[b]
sbox[b] = temp
c = (sbox[a] + sbox[b]) % 256
# 明文字节由子密钥异或加密
temp = ord(src[i]) ^ sbox[c]
# 密文字节转换成hex,格式对齐修正(取最后两位,若为一位([0x0,0xF]),则改成[00, 0F])
temp = str(hex(temp))[-2:]
temp = temp.replace('x', '0')
# 输出
output += temp
return output
# 请求网址
url = "http://2.2.2.3/ac_portal/login.php"
# 请求头
headers = {
"X-Requested-With": "XMLHttpRequest",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", # 必须指定,否则报404
}
# 时间戳(提取ms单位)
tag = int(time.time()*1000)
# 利用RC4加密算法获取基于时间戳的密码
pwd = do_encrypt_rc4(password, str(tag))
# 账号、密码、时间戳写入payload报文
payload = f"opr=pwdLogin&userName={account}&pwd={pwd}&auth_tag={tag}&rememberPwd=1"
while True:
try:
# 提交登录
res = requests.post(url, data=payload, headers=headers)
# 输出登录结果
if res.status_code == 200 and res.text.find("true")>0:
print(f"\033[7;32;47m{res.content.decode('utf-8')} \033[0m")
else:
print(f"\033[7;31;47m{res.content.decode('utf-8')} \033[0m")
print("\033[7;31;47m", "Login fail! Make sure input true account info!", "\033[0m")
os.remove(file_name)
print("\033[7;36;47m", "Already clear account file.\n Restart EXE and input again.", "\033[0m")
print("Press any key to exit...")
# 等待退出
if ord(msvcrt.getch()):
break
# 如果请求出错,大概率网络未连
except Exception as err:
# 提示
print("\033[7;31;47m","Login Error!\tMaybe you need link wifi first?" ,"\033[0m")
# 输出err
print("\033[7;33;40m", err,"\033[0m")
# 按“R”再次执行脚本,按其它按键退出
print("Press 'R' to restart, or break...")
if ord(msvcrt.getch()) != 114:
break
三、PC 部署
由于这里大部分的PC都是基于windows系统的,所以这里以windows系统为例进行讲解。
windows下脚本的部署关键是实现开机自动执行。在windows下有一个自启动文件夹:
C:\Users\你的用户名称\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
这个文件夹里放置的所有windows可识别文件都将在开机时自动执行,这对我们自动化脚本是个非常有用的。下面我们就使用这个文件夹进行部署。
1. py 脚本部署
最简单的当然是直接以py脚本进行部署了。但是这也是有条件的,这需要电脑里有python编译器支持。
我们可以写一个bat批处理文件,调用python编译器执行脚本:
start python /.../login_network.py
注意,这里py文件的位置最好不要也放在同级Startup目录下,尤其是已经指定了默认打开py文件的软件。这会让导致开机的时候以指定的软件打开py文件。(八成你执行的脚本变成了VS里等待编写的代码)
另外环境变量里需要配置python,否则python指令无法被windows系统识别。
然后,只需要将bat文件放在前面说的文件夹下即可。
2. EXE 执行文件部署
为了更好的通用性,可以选择将脚本打包成exe可执行文件,这样即使没有python编译器也可以在任意windows系统pc执行。但缺点就是因为打包了python编译器,导致一个简单的脚本也需要很多mb的空间占用。
首先,我们安装Auto PY to EXE,这是一个基于PyInstaller实现的.py到.exe的转换器,拥有简单的图形界面。
pip install auto-py-to-exe
这里最好在新的虚拟环境里安装,因为
pyinstaller
可能会因为不纯净的环境以及过多的库导致打包占用很大。
然后切换到对应的安装环境。先关闭浏览器,再直接在终端运行:
auto-py-to-exe
就可以打开基于浏览器的打包UI:
图10. auto-py-to-exe界面
我们选择单文件打包方式,选择基于控制台窗口的输出,添加图标等其它要素后打包。结果如下:
图11. auto-py-to-exe打包结果
可以看到右侧就是我们输出的exe文件。文件大小还是有点大的,本来1kb的代码打了个包变成了8.53mb。
图12. 打包大小
打包完我们将代码放到自启动目录。无需其它操作,开机即可自行启动运行。
3. 首次启动
无论使用哪种部署方式,部署完成后都需要先启动一次,生成本地账号密码。
根据提示输入账号密码后,如果输入正确,则会绿色反白显示正确报文,运行结果如图所示:
图13. 第一次启动运行正确效果
如果输出错误,则文件不会生成,并且红色反白输出错误报文,需要重新运行并再次输入正确的账号密码:
图14. 第一次启动运行错误效果
四、Android部署
除了PC端,安卓也可以完成部署。只需要下载一个QPython3的app用来模拟linux环境,然后在其中运行python脚本即可。
这个软件可以在Google商店或者官网下载,软件界面如图所示:
图15. QPython3界面效果
我们将脚本拷入其程序默认存储文件夹,然后点击运行脚本,按照一样的步骤,也可以得到正确的结果。
这里的脚本msvcrt库不能适配模拟的linux,所以修改或者删掉相关代码即可。
图16. QPython3脚本运行效果
由于移动端的特性不同,我们没有必要实现开机自动执行。只需要设置默认启动脚本后,在链接wifi时点击软件主界面上方的那个python标志即可一键联网。
五、总结
本文介绍了模拟浏览器联网的自动脚本的实现过程,通过分析前端报文获得加密原理然后模拟加密过程,再将加密结果模拟浏览器向服务器提交,实现无需操作的自动联网。简化了每天的日常联网活动,节约了时间。
免责声明:本文仅作为技术探讨,对于基于本文技术进行的任何违规违法行为,均与本人无关。
如有疑问或错误,欢迎和我私信交流指正。
版权所有,未经授权,请勿转载!
更多推荐
所有评论(0)