Python模拟登录ISP路由器失败 加密POST请求返回200状态码
GPON家庭网关Python自动登录脚本故障修复
问题背景
- 运营商配发的GPON家庭网关未开放SSH、Telnet管理权限,仅支持Web端凭用户凭证登录
- 需求为编写Python脚本实现固定间隔登录路由器并执行重启操作
- 复现前端JS加密登录流程时,请求返回200状态码(预期登录成功返回299状态码,同时下发认证Sid Cookie),响应内容为登录页源码,认证失败
故障根因
代码存在4处和前端加密/请求逻辑不匹配的问题:
- RSA加密填充模式错误:前端使用JSEncrypt库实现RSA加密,默认采用PKCS#1 v1.5填充规则,原代码使用PKCS1_OAEP填充,后端解密时填充校验不通过直接判定密文非法
- Base64编码规则不匹配:两个加密字段的编码规则存在差异,原代码未做区分:
ct字段(AES密文)前端使用SJCL的base64url编码:+转-、/转_、直接删除末尾的=填充符ck字段(RSA加密的AES密钥信息)前端使用自定义base64url_escape编码:+转-、/转_、=转.
- 会话Cookie被覆盖:首次访问首页时服务端会下发会话标识,原代码发送POST登录请求时手动重置Cookie,导致前后请求会话不连续,服务端无法关联token、nonce等上下文参数
- (次要)原代码随机数生成逻辑虽然字节长度正确,但未严格对齐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
相关产品推荐
相关产品推荐

