JS逆向:某上网行为认证设备登录密码认证接口分析

某上网行为认证设备登录密码认证接口分析

免责声明: 本文仅用于 JavaScript 逆向、RC4 加密算法与接口协议的学习研究,所有分析内容均来自前端公开代码。文中逻辑与代码仅作技术演示,请勿用于未经授权的自动登录、批量操作或其他违反网络管理规定及国家法律法规的行为,一切后果由使用者自行承担。

1. 登录接口

前端通过如下接口提交密码认证请求:

POST /ac_portal/login.php

请求参数中使用:

opr=pwdLogin

表示当前采用的是账号密码登录方式。


2. 关键前端代码

2.1 密码登录逻辑

function onPwdLogin() {
    if (!pwdValidtor()) return;
    if ($id("flux")) {
        $id("flux").href = "https://" + document.domain + "/cgi-bin/showflux.cgi?user=" + encodeURIComponent($id("password_name").value);
    }
    var rckey = +(new Date()) + '';
    var pwd = do_encrypt_rc4($id("password_pwd").value, rckey);
    var params = {
        opr: 'pwdLogin',
        userName: $id("password_name").value,
        pwd : pwd,
        auth_tag: rckey,
        rememberPwd: $id("rememberPwd").checked ? '1' : '0'
    };
    if (g_WXscanOpenID) {
        params.openid = g_WXscanOpenID;
    }
    if (g_wxscan_mobile_openid) {
        params.openid = g_wxscan_mobile_openid;
    }
    loginRequest(params, "mode_password", $id("password_submitBtn"));
}

3. 参数含义分析

根据 onPwdLogin() 可知,密码登录阶段关键参数如下:

  • opr: 'pwdLogin'

    • 指定登录操作类型为密码登录
  • userName

    • 用户名
  • pwd

    • 前端加密后的密码,不是明文
  • auth_tag

    • 当前毫秒时间戳字符串
    • 同时作为密码加密所用的密钥
  • rememberPwd

    • 是否记住密码,1 为记住,0 为不记住

4. 密码加密方式

核心代码如下:

var rckey = +(new Date()) + '';
var pwd = do_encrypt_rc4($id("password_pwd").value, rckey);

可得出以下结论:

rckey = 当前毫秒时间戳字符串
pwd   = do_encrypt_rc4(明文密码, rckey)
auth_tag = rckey

即:

pwd = RC4(明文密码, auth_tag)

因此提交时的 auth_tag 不仅仅是一个普通时间戳参数,而是实际参与密码加密的密钥。


5. RC4 加密函数

前端定义如下:

function do_encrypt_rc4(src, passwd) {
    src = $.trim(src+'');
    passwd = passwd + '';
    var i, j = 0, a = 0, b = 0, c = 0, temp;
    var plen = passwd.length,
        size = src.length;

    var key = Array(256);
    var sbox = Array(256);
    var output = Array(size);

    for (i = 0; i < 256; i++) {
        key[i] = passwd.charCodeAt(i % plen);
        sbox[i] = i;
    }
    for (i = 0; i < 256; i++) {
        j = (j + sbox[i] + key[i]) % 256;
        temp = sbox[i];
        sbox[i] = sbox[j];
        sbox[j] = temp;
    }
    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];
        temp = temp.toString(16);
        if (temp.length === 1) {
            temp = '0' + temp;
        } else if (temp.length === 0) {
            temp = '00';
        }
        output[i] = temp;
    }
    return output.join('');
}

该函数的特点:

  1. 使用 passwd 作为 RC4 密钥初始化 keysbox
  2. 对输入明文 src 做逐字节异或
  3. 输出结果转为十六进制字符串
  4. 每个字符对应 2 位十六进制

例如若密码长度为 6 位,则输出长度通常为 12 位十六进制字符串。


6. 为什么同一个密码每次抓包结果不同

因为:

var rckey = +(new Date()) + '';

每次登录都会生成新的毫秒时间戳,所以:

  • 明文密码相同
  • auth_tag 不同
  • pwd 也不同

这解释了抓包中同一密码出现不同加密结果的现象。


7. 后端校验方式推断

前端提交的数据为:

userName = 用户名
pwd      = RC4加密后的密码
auth_tag = 时间戳字符串

因此后端大概率会:

  1. 读取 auth_tag
  2. auth_tag 作为同样的 RC4 密钥
  3. pwd 做还原或等价校验
  4. 验证结果是否与真实密码匹配

所以 auth_tag 实际上承担了“会话级临时密钥”的作用。


8. 修改密码逻辑也采用同样机制

onChangePwd() 中同样使用了相同的方式:

function onChangePwd() {
    if (!changePwdValidtor()) return;
    var rckey = +(new Date()) + '';
    var oldPwd = do_encrypt_rc4($id("changePwd_oldPwd").value, rckey);
    var newPwd = do_encrypt_rc4($id("changePwd_newPwd").value, rckey);
    var params = {
        opr: 'changePwd',
        userName: $id("changePwd_name").value,
        auth_tag: rckey,
        oldPwd:oldPwd,
        newPwd:newPwd
    };
    changePwdRequest(params, "mode_changePwd", $id("changePwd_submitBtn"))
}

说明该 Portal 在涉及密码提交的场景中,统一采用:

时间戳作为 RC4 密钥

来保护明文密码。


9. 最终结论

该认证系统的密码字段:

  • 不是明文
  • 不是固定 MD5
  • 不是固定密钥加密
  • 而是:
pwd = RC4(明文密码, 当前毫秒时间戳字符串)

并且:

auth_tag = 当前毫秒时间戳字符串

最终随请求一起提交:

opr=pwdLogin
userName=用户名
pwd=RC4加密结果
auth_tag=时间戳
rememberPwd=0/1

10. 自动化实现结论

由于加密逻辑已经明确,因此可以用脚本稳定复现登录过程:

自动登录流程

  1. 获取当前毫秒时间戳
  2. 将时间戳转为字符串,作为 auth_tag
  3. 使用相同 RC4 算法对明文密码加密
  4. userNamepwdauth_tag 提交到 /ac_portal/login.php
  5. 解析返回结果,判断登录是否成功

这意味着该认证过程可以被实现为:

  • 加密逻辑可通过编程复现,用于学习接口协议与加密流程。

11.自动化实现伪代码

#!/usr/bin/env python3
import time
import sys
import requests

# 注意:以下为演示地址,非真实接口
URL = "http://xxx.xxx.xxx.xxx/ac_portal/login.php"
# 注意:以下为示例账号密码,非真实信息
USERNAME = "your_username_example"
PASSWORD = "your_password_example"

HEADERS = {
    "User-Agent": "Mozilla/5.0",
    "Accept-Encoding": "gzip, deflate",
    "X-Requested-With": "XMLHttpRequest",
    "DNT": "1",
    "Origin": "http://xxx.xxx.xxx.xxx",
    "Referer": "http://xxx.xxx.xxx.xxx/ac_portal/",
    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
}


def do_encrypt_rc4(src, passwd):
    """
    RC4加密算法实现(仅用于技术演示)
    :param src: 待加密字符串
    :param passwd: 加密密钥
    :return: 十六进制加密结果
    """
    src = str(src).strip()
    passwd = str(passwd)

    plen = len(passwd)
    size = len(src)

    key = [0] * 256
    sbox = [0] * 256
    output = []

    # 初始化密钥流
    for i in range(256):
        key[i] = ord(passwd[i % plen])
        sbox[i] = i

    j = 0
    for i in range(256):
        j = (j + sbox[i] + key[i]) % 256
        sbox[i], sbox[j] = sbox[j], sbox[i]

    # 加密核心逻辑
    a = 0
    b = 0
    for i in range(size):
        a = (a + 1) % 256
        b = (b + sbox[a]) % 256
        sbox[a], sbox[b] = sbox[b], sbox[a]
        c = (sbox[a] + sbox[b]) % 256
        temp = ord(src[i]) ^ sbox[c]
        output.append(f"{temp:02x}")

    return "".join(output)


def login_demo():
    """
    登录逻辑演示
    """
    # 生成时间戳作为加密密钥
    auth_tag = str(int(time.time() * 1000))
    # RC4加密密码
    pwd = do_encrypt_rc4(PASSWORD, auth_tag)

    data = {
        "opr": "pwdLogin",
        "userName": USERNAME,
        "pwd": pwd,
        "auth_tag": auth_tag,
        "rememberPwd": "0",
    }

    try:
        # 仅为逻辑演示,实际运行需替换为合法的目标地址
        r = requests.post(URL, headers=HEADERS, data=data, timeout=10)
        print(f"HTTP {r.status_code}")
        print(r.text)

        if r.ok:
            text = r.text.lower()
            if "success" in text or '"success":true' in text or '"success":1' in text:
                return 0
    except Exception as e:
        print(f"演示示例:请求异常 - {e}")
        return 1

    return 1


if __name__ == "__main__":
    """
    免责声明:
    1. 本代码仅用于学习RC4加密算法和JS逆向技术,请勿用于任何非法用途
    2. 代码中所有接口、账号密码均为示例,无实际使用价值
    3. 使用本代码需遵守所在单位/学校的网络安全规定及相关法律法规
    """
    sys.exit(login_demo())