前言

 研一这几个月,每天最头疼的事莫过于联网还得登录。作为一个懒人,如此的重复劳动属实是不能接受的。所以,为了解决这个问题,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函数代码段

果然,历经千山万水,找到了你呀~

这个函数才是最终加密的地方,用户输入的原始密码和毫秒时间戳分别作为srcpasswd参数输入函数并最终处理返回一串加密字符串。而这里使用的算法是大名鼎鼎的RC4加密算法(好吧,虽然我没用过但听说挺有名的:happy:)。

3. RC4加密及实现原理

参考这篇介绍RC4加密算法比较清楚的文章:RC4加密算法原理简单理解,简单分析一下这里的加密原理:


图9. RC4加密算法流程图

  1. 初始化密钥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;
    }
    
  2. 对状态向量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在经过伪随机子密码(这里就是时间戳)生成算法的处理后可以得到不同的子密钥序列,并且,该序列是随机的。

  3. 秘钥流的生成与加密

    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标志即可一键联网。

五、总结

本文介绍了模拟浏览器联网的自动脚本的实现过程,通过分析前端报文获得加密原理然后模拟加密过程,再将加密结果模拟浏览器向服务器提交,实现无需操作的自动联网。简化了每天的日常联网活动,节约了时间。


免责声明:本文仅作为技术探讨,对于基于本文技术进行的任何违规违法行为,均与本人无关。

如有疑问或错误,欢迎和我私信交流指正。
版权所有,未经授权,请勿转载!

Copyright © 2022 by YuxiChen. All rights reserved.
Logo

更多推荐