You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python模拟登录ISP路由器失败 加密POST请求返回200状态码

GPON家庭网关Python自动登录脚本故障修复

问题背景

  • 运营商配发的GPON家庭网关未开放SSH、Telnet管理权限,仅支持Web端凭用户凭证登录
  • 需求为编写Python脚本实现固定间隔登录路由器并执行重启操作
  • 复现前端JS加密登录流程时,请求返回200状态码(预期登录成功返回299状态码,同时下发认证Sid Cookie),响应内容为登录页源码,认证失败

故障根因

代码存在4处和前端加密/请求逻辑不匹配的问题:

  1. RSA加密填充模式错误:前端使用JSEncrypt库实现RSA加密,默认采用PKCS#1 v1.5填充规则,原代码使用PKCS1_OAEP填充,后端解密时填充校验不通过直接判定密文非法
  2. Base64编码规则不匹配:两个加密字段的编码规则存在差异,原代码未做区分:
    • ct字段(AES密文)前端使用SJCL的base64url编码:+转-、/转_、直接删除末尾的=填充符
    • ck字段(RSA加密的AES密钥信息)前端使用自定义base64url_escape编码:+转-、/转_、=转.
  3. 会话Cookie被覆盖:首次访问首页时服务端会下发会话标识,原代码发送POST登录请求时手动重置Cookie,导致前后请求会话不连续,服务端无法关联token、nonce等上下文参数
  4. (次要)原代码随机数生成逻辑虽然字节长度正确,但未严格对齐SJCL输出格式,不过AES密钥、IV仅要求长度为16字节即可正常使用,该问题不影响解密流程

修复后可运行代码

import requests
import re
import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import AES, PKCS1_v1_5
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad
from urllib import parse
from config import username, password
import warnings
import time
warnings.filterwarnings('ignore')

def gen_random_16b():
    # 生成16字节随机数,对应SJCL randomWords(4,0)的输出长度
    return get_random_bytes(16)
    
def b64_escape_for_ck(b64_bytes):
    # 适配ck字段的编码规则:+→- /→_ =→.
    b64_str = b64_bytes.decode('utf-8')
    out = []
    for c in b64_str:
        if c == '+':
            out.append('-')
        elif c == '/':
            out.append('_')
        elif c == '=':
            out.append('.')
        else:
            out.append(c)
    return ''.join(out)

def b64url_for_ct(data_bytes):
    # 适配ct字段的编码规则:标准urlsafe base64后去掉末尾=
    return base64.urlsafe_b64encode(data_bytes).decode('utf-8').rstrip('=')

def encrypt_post_data(pubkey_str, plaintext):
    # AES-CBC加密明文
    aeskey = gen_random_16b()
    iv = gen_random_16b()
    pt = pad(plaintext.encode('utf-8'), AES.block_size)
    aes_cipher = AES.new(aeskey, AES.MODE_CBC, iv=iv)
    ct = aes_cipher.encrypt(pt)
    
    # RSA加密AES密钥+IV信息,使用PKCS1_v1_5填充匹配JSEncrypt逻辑
    rsa_key = RSA.import_key(pubkey_str)
    rsa_cipher = PKCS1_v1_5.new(rsa_key)
    aes_info = base64.b64encode(aeskey).decode('utf-8') + ' ' + base64.b64encode(iv).decode('utf-8')
    ck = rsa_cipher.encrypt(aes_info.encode('utf-8'))
    
    return {
        'encrypted': '1',
        'ct': b64url_for_ct(ct),
        'ck': b64_escape_for_ck(base64.b64encode(ck))
    }


def main():
    base_url = 'https://192.168.1.254/'
    with requests.Session() as session:
        # 首次访问首页,获取动态参数与会话Cookie
        init_headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
            'Accept-Language': 'en-GB,en;q=0.5',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1',
        }
        resp = session.get(base_url, headers=init_headers, verify=False)
        resp.raise_for_status()

        # 从首页源码提取动态加密参数
        pubkey_match = re.findall(r"var pubkey = \'[\S\s]+\n\'", resp.text)[0]
        pubkey = re.sub(r"\\", "", pubkey_match.split("'")[-2])
        nonce = re.findall(r"var nonce = \"[\S]+\"", resp.text)[0].split('"')[-2]
        token = re.findall(r"var token =\"[\S]+\"", resp.text)[0].split('"')[-2]

        # 构造待加密的明文参数
        dec_key = b64_escape_for_ck(base64.b64encode(gen_random_16b()))
        dec_iv = b64_escape_for_ck(base64.b64encode(gen_random_16b()))
        post_plain = f'&username={username}&password={parse.quote(password)}&csrf_token={token}&nonce={nonce}&enckey={dec_key}&enciv={dec_iv}'
        
        # 加密参数
        login_data = encrypt_post_data(pubkey, post_plain)

        # 发送登录请求,不要手动覆盖Cookie,使用Session自动维护的会话
        login_headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0',
            'Accept': '*/*',
            'Accept-Language': 'en-GB,en;q=0.5',
            'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
            'X-Requested-With': 'XMLHttpRequest',
            'Origin': 'https://192.168.1.254',
            'Connection': 'keep-alive',
        }
        time.sleep(1)
        login_resp = session.post(base_url + 'login.cgi', headers=login_headers, data=login_data, verify=False)
        print(f"登录响应状态码:{login_resp.status_code}")
        if login_resp.status_code == 299:
            print("登录成功,当前会话Cookie:", session.cookies.get_dict())
            # 后续在此处添加重启路由器的请求逻辑,重启接口可通过浏览器抓包获取
        else:
            print("登录失败,响应内容:", login_resp.text[:200])

if __name__ == '__main__':
    main()

注意事项

  • 每次登录前必须重新从首页源码提取pubkey、nonce、csrf_token三个动态参数,这类参数每次刷新页面都会重新生成,使用固定值会直接认证失败
  • 登录成功后Session会自动保存认证Cookie,后续重启等操作直接用同一个Session发起请求即可,无需重复处理Cookie
  • 重启接口的路径和参数可以通过浏览器开发者工具抓包获取,一般为/reboot.cgi,发送对应GET/POST请求即可完成重启操作

内容的提问来源于stack exchange,提问作者dkpro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 17:39:18