title: 某校园网认证api分析
author: lensfrex
cover: 'https://oss-img.ciduid.top/blog/covers/61391031_p0_compressed.png'
tags: 
    - 网络
categories: 
    - 网络
date: 2022-07-06 13:22:00
---

> 封面：https://www.pixiv.net/artworks/61391031

---

*最近突然来了兴趣，想搞一搞校园网认证页的认证api，方便以后自动登录，而不是每次联网都要手动登陆一遍*

*虽然我这里可以直接用拨号登录，而且我已经用上了路由器，直接全程连接，更加方便，但是还是想搞一搞这个登录认证*

*这里只是在研究认证页的时候的随手记，如果直接看的话可能会一脸懵逼，等有空了我再补充详细的过程吧（咕）*

---
> ### 后续来啦：[这里](https://www.ciduid.top/2022/1113/srun-login-analyze/)
---

学校校园网登录认证的过程大致分为两步

## 1. 获取challenge码

这部分简单，直接发学号用户名（学号）就行了

Base URL：`http://59.68.177.183/cgi-bin/get_challenge`

方法：GET

请求参数：

``` apache
callback: jQuery112409362602503309623_1657072809873
username: 【学号】
ip: 10.15.2.109
_: 1657072809875

```
callback: 回调函数，后半部分为时间戳，不设置callback的话只有ok

username: 顾名思义

ip:       顾名思义

_:        时间戳

注意：这里的时间戳是GMT时间，而不是咱们的东八区时间

服务器返回数据，有用的是`challenge`

``` apache
challenge: "4d8e7d882e52c17537e1203191a8308fe294520bfb8e15c1f860bd1b03d983cc"
client_ip: "10.15.2.109"
ecode: 0
error: "ok"
error_msg: ""
expire: "60"
online_ip: "10.15.2.109"
res: "ok"
srun_ver: "SRunCGIAuthIntfSvr V1.18 B20211105"
st: 1657074027
```
---

## 2. 根据challenge码进行验证

这部分就有点复杂了，但还好，浏览器控制台跳几跳就看得出来是怎样的了

Base URL：`http://59.68.177.183/cgi-bin/srun_portal`

方法：GET

请求参数

``` apache
callback: jQuery112409362602503309623_1657072809873
action: login
username: 【学号】
password: {MD5}a25f5a06d8484669fb3ca3df0362efc8
os: Windows 10
name: Windows
double_stack: 0
chksum: be73bd397721c3f39392e3fbfcc8124be9d0516a
info: {SRBX1}F1DdbTEkXB8FyHKlvt+GnvkjogeyV6HZst2G+VWWo684jx/32muHiDv3fKLh9AwIoaUCsu2rRaLlOB2Te5A4lYSFqCPLGJizZ5KsBUX5t6tNcN6coDACovbe4Pv0fF0pulYtqpy+i6D=
ac_id: 7
ip: 10.15.2.109
n: 200
type: 1
_: 1657072809876
```

`password`字段的MD5由明文密码和上面得到的`challenge`码拼接之后得到的MD5

``` apache
ServerFlag: 0
ServicesIntfServerIP: "172.26.23.173"
ServicesIntfServerPort: "8001"
access_token: "4d8e7d882e52c17537e1203191a8308fe294520bfb8e15c1f860bd1b03d983cc"
checkout_date: 0
client_ip: "10.15.2.109"
ecode: 0
error: "ok"
error_msg: ""
online_ip: "10.15.2.109"
ploy_msg: "E0000: Login is successful."
real_name: ""
remain_flux: 0
remain_times: 0
res: "ok"
srun_ver: "SRunCGIAuthIntfSvr V1.18 B20211105"
suc_msg: "login_ok"
sysver: "1.01.20211105"
username: "【学号】"
wallet_balance: 0
```

在这里把登录认证发送部分的js代码挂上来（没有完全混淆代码而且还有注释，太棒啦）：

这里的`token`即是之前获取到的`challenge`

请求部分的代码在此：
<details>
<summary>查看完整代码</summary>

```javascript
_loginAccount.set(_assertThisInitialized(_this), {
            writable: true,
            value: function value(obj) {
                    // 加密常量
                    var type = 1;
                    var n = 200;
                    var enc = 'srun_bx1';
                    // 用户信息

                    var username = _this.userInfo.username + _this.userInfo.domain;
                    var password = _this.userInfo.password;
                    var ac_id = _this.portalInfo.acid;
                    // 正在等待中的请求

                    var pendingReqNum = 0;
                    // 请求成功的消息

                    var successMsg = '';
                    // 发起认证方法

                    var sendAuth = function sendAuth() {
                        var host = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
                        // 双栈认证时 IP 参数为空
                        var ip = _this.portalInfo.doub && host ? '' : _this.userInfo.ip;
                        // 获取 Token

                        _classPrivateFieldGet(_assertThisInitialized(_this), _getToken)
                            .call(_assertThisInitialized(_this), host, ip, function(token) {
                                // 用户密码 MD5 加密
                                var hmd5 = md5(password, token);
                                // 用户信息加密

                                var i = _classPrivateFieldGet(_assertThisInitialized(_this), _encodeUserInfo).call(_assertThisInitialized(_this), {
                                        username: username,
                                        password: password,
                                        ip: ip,
                                        acid: ac_id,
                                        enc_ver: enc
                                    }, token);

                                var str = token + username;
                                str += token + hmd5;
                                str += token + ac_id;
                                str += token + ip;
                                str += token + n;
                                str += token + type;
                                str += token + i;
                                // 防止 IPv6 请求网络不通进行 try catch

                                try {
                                    pendingReqNum += 1;
                                    // 发起认证请求

                                    _this.ajax.jsonp({
                                        host: host,
                                        url: _classPrivateFieldGet(_assertThisInitialized(_this), _api)
                                            .auth,
                                        params: {
                                            action: 'login',
                                            username: username,
                                            password: _this.userInfo.otp ? '{OTP}' + password : '{MD5}' + hmd5,
                                            os: _this.portalInfo.userDevice.device,
                                            name: _this.portalInfo.userDevice.platform,
                                            // 未开启双栈认证，参数为 0
                                            // 开启双栈认证，向 Portal 当前页面 IP 认证时，参数为 1
                                            // 开启双栈认证，向 Portal 另外一种 IP 认证时，参数为 0
                                            double_stack: _this.portalInfo.doub && !host ? 1 : 0,
                                            chksum: sha1(str),
                                            info: i,
                                            ac_id: ac_id,
                                            ip: ip,
                                            n: n,
                                            type: type
                                        },
                                        success: function success(res) {
                                            pendingReqNum -= 1;
                                            // 认证成功，用户上线

                                            _this.online = true;
                                            // 更改登录状态为结束

                                            _this.running.login = false;
                                            // IP 已经在线了 - 给出提示

                                            if (res.suc_msg === 'ip_already_online_error' && obj.error)
                                                return _this.confirm({
                                                    message: _this.translate('ip_already_online_error'),
                                                    confirm: function confirm() {
                                                        if (obj.error)
                                                            obj.error();
                                                    }
                                                });
                                            // 翻译后的认证成功信息

                                            successMsg = _this.translate(res);
                                        },
                                        error: function error(res) {
                                            pendingReqNum -= 1;
                                            // 更改登录状态为结束

                                            _this.running.login = false;
                                            // 若 ecode 为 E2620 且开启了在线设备管理功能
                                            // E2620: 超出允许的在线数目

                                            if (res.ecode === 'E2620' && CREATER.useOnlineDeviceMgr)
                                                return _this.confirm({
                                                    message: _this.translate('E2620Tips'),
                                                    confirm: function confirm() {
                                                        _this.dialog.open('onlineDeviceMgr', function() {
                                                            _this.getOnlineDevice();
                                                        });
                                                    }
                                                });
                                            // IP 已经在线了 - 重新认证

                                            if (res.error_msg === 'ip_already_online_error')
                                                return _this.reAuth(obj);
                                            // 需要查询日志的情况

                                            if (res.error_msg === 'not_online_error')
                                                return _this.showLog();
                                            if (res.error_msg === 'no_response_data_error')
                                                return _this.showLog();
                                            if (res.error_msg === 'RD000')
                                                return _this.showLog();
                                            // 若提示修改密码

                                            if (res.error_msg === 'user_must_modify_password')
                                                return _this.confirm({
                                                    message: _this.translate(res),
                                                    confirmText: _this.translate('ToChangePassword'),
                                                    confirm: function confirm() {
                                                        return $('#forget')
                                                            .click();
                                                    },
                                                    cancel: function cancel() {}
                                                });
                                            // 错误提示

                                            _this.confirm({
                                                message: _this.translate(res),
                                                confirm: function confirm() {
                                                    if (obj.error)
                                                        obj.error(res);
                                                }
                                            });
                                        }
                                    });
                                } catch (err) {
                                    // 因为 IPv6 网络问题导致的认证失败
                                    pendingReqNum -= 1;
                                }
                            });
                    };
```
</details>

上面的`portalInfo`是从`config`来的，而`config`定义在登录认证首页下面：

``` javascript
    var CONFIG = {
        page   : 'account',
        ip     : "10.15.2.109",
        nas    : "",
        mac    : "",
        url    : "",
        lang   : "zh-CN" || 'zh-CN',
        isIPV6 :  false ,
        portal : {"AuthIP":"","AuthIP6":"","ServiceIP":"https://59.68.177.181:8800","DoubleStackPC":false,"DoubleStackMobile":false,"AuthMode":false,"CloseLogout":false,"MacAuth":false,"RedirectUrl":true,"OtherPCStack":"IPV4","OtherMobileStack":"IPV4","MsgApi":"new","PublicSuccessPages":true,"TrafficCarry":1000,"UserAgreeSwitch":false,"DialSwitch":false},
        notice : "list",
        priceList : {"Prices":"5,10,20,50,100","Default":"0.01","BalanceWarning":"5"}
    };
```

info字段的数据是在下面这里生成的，关键是encode函数，具体是什么算法看不出来

encode函数生成一段神奇的编码数据之后再用一种动过手脚的base64编码一下就得到了上面的i变量

把这段代码算法转成其他其他语言就好办很多了

<details>
<summary>展开查看代码（js）</summary>

``` javascript
_encodeUserInfo.set(_assertThisInitialized(_this), {
            writable: true,
            value: function value(info, token) {
                // 克隆自 $.base64，防止污染
                var base64 = _this.clone($.base64);
                // base64 设置 Alpha

                base64.setAlpha('LVoJPiCN2R8G90yg+hmFHuacZ1OWMnrsSTXkYpUq/3dlbfKwv6xztjI7DeBE45QA');
                // 用户信息转 JSON

                info = JSON.stringify(info);

                function encode(str, key) {
                    if (str === '')
                        return '';
                    var v = s(str, true);
                    var k = s(key, false);
                    if (k.length < 4)
                        k.length = 4;
                    var n = v.length - 1, z = v[n], y = v[0], c = 0x86014019 | 0x183639A0, m, e, p, q = Math.floor(6 + 52 / (n + 1)), d = 0;

                    while (0 < q--) {
                        d = d + c & (0x8CE0D9BF | 0x731F2640);
                        e = d >>> 2 & 3;

                        for (p = 0; p < n; p++) {
                            y = v[p + 1];
                            m = z >>> 5 ^ y << 2;
                            m += y >>> 3 ^ z << 4 ^ (d ^ y);
                            m += k[p & 3 ^ e] ^ z;
                            z = v[p] = v[p] + m & (0xEFB8D130 | 0x10472ECF);
                        }

                        y = v[0];
                        m = z >>> 5 ^ y << 2;
                        m += y >>> 3 ^ z << 4 ^ (d ^ y);
                        m += k[p & 3 ^ e] ^ z;
                        z = v[n] = v[n] + m & (0xBB390742 | 0x44C6F8BD);
                    }

                    return l(v, false);
                }

                function s(a, b) {
                    var c = a.length;
                    var v = [];

                    for (var i = 0; i < c; i += 4) {
                        v[i >> 2] = a.charCodeAt(i) | a.charCodeAt(i + 1) << 8 | a.charCodeAt(i + 2) << 16 | a.charCodeAt(i + 3) << 24;
                    }

                    if (b)
                        v[v.length] = c;
                    return v;
                }

                function l(a, b) {
                    var d = a.length;
                    var c = d - 1 << 2;

                    if (b) {
                        var m = a[d - 1];
                        if (m < c - 3 || m > c)
                            return null;
                        c = m;
                    }

                    for (var i = 0; i < d; i++) {
                        a[i] = String.fromCharCode(a[i] & 0xff, a[i] >>> 8 & 0xff, a[i] >>> 16 & 0xff, a[i] >>> 24 & 0xff);
                    }

                    return b ? a.join('').substring(0, c) : a.join('');
                }

                return '{SRBX1}' + base64.encode(encode(info, token));
            }
        });
```
</details>

用go简单实现了一下这段加密算法：

<details>
<summary>展开查看代码（go）</summary>

```go
import (
	"encoding/base64"
	"math"
)

// 这里的base64和正常的base64编码是不一样的，这里的base64并不按照普通的ABCD字母顺序来对应相应字节，而是按照以下字母表对应字节
const magicBase64Alpha = "LVoJPiCN2R8G90yg+hmFHuacZ1OWMnrsSTXkYpUq/3dlbfKwv6xztjI7DeBE45QA"

var magicBase64 = base64.NewEncoding(magicBase64Alpha)

func Encode(str string, key string) string {
	encodeResult := encode(str, key)
	base64Result := magicBase64.EncodeToString(int32ToAsciiBytes(encodeResult))

	return "{SRBX1}" + base64Result
}

// encode 直接照着原网页的js代码改的，只是能跑
// 因为各种类型转换从理论上来说性能可能会差了点，但是在这里的使用情景几乎是感觉不到的
// 难度其实主要还是在于动态类型语言和静态类型语言在运算时数据溢出的问题不好处理
func encode(str string, key string) []int32 {
	v := magicEncode(str, true)
	k := magicEncode(key, false)

	if len(k) < 4 {
		for i := 0; i < (4 - len(k)); i++ {
			k = append(k, 0)
		}
	}

	n := int32(len(v) - 1)
	z := v[n]
	y := v[0]
	c := int32(-1640531527)
	q := int(math.Floor(float64(6+52/(n+1)))) - 1
	d := int32(0)
	var e, p int32

	// 这里的m用int64（long）是因为后面+=的时候int32会溢出
	// 其他变量不用int64是因为直接用int64会导致位运算错误，和js的运算结果不一致
	var m int64

	for ; q >= 0; q-- {
		d = d + c&(-1)
		e = uRightShift(d, 2) & 3

		for p = 0; p < n; p++ {
			y = v[p+1]
			m = int64(uRightShift(z, 5) ^ y<<2)
			m += int64(uRightShift(y, 3) ^ z<<4 ^ (d ^ y))
			m += int64(k[p&3^e] ^ z)
			v[p] = v[p] + int32(m&(-1))
			z = v[p]
		}

		y = v[0]
		m = int64(uRightShift(z, 5) ^ y<<2)
		m += int64(uRightShift(y, 3) ^ z<<4 ^ (d ^ y))
		m += int64(k[p&3^e] ^ z)
		v[n] = v[n] + int32(m&(-1))
		z = v[n]
	}

	return magicDecode(v, false)
}

// 神秘的“初步”加密代码，把字符串一四个字符为一组转成神秘的int32数组
func magicEncode(source string, sizeOnLast bool) (result []int32) {
	data := []int32(source)
	dataLen := len(data)

	resultLen := dataLen / 4
	if sizeOnLast {
		result = make([]int32, resultLen+1)
		result[resultLen] = int32(dataLen)
	} else {
		result = make([]int32, resultLen)
	}

	for i := 0; i < dataLen; i += 4 {
		result[i>>2] = get(data, i, dataLen) | get(data, i+1, dataLen)<<8 | get(data, i+2, dataLen)<<16 | get(data, i+3, dataLen)<<24
	}

	return result
}

//和上面的是反过来的
func magicDecode(data []int32, sizeOnLast bool) (result []int32) {
	dataLength := len(data)

	c := dataLength - 1<<2

	if sizeOnLast {
		m := int(data[dataLength-1])
		if m < c-3 || m > c {
			return nil
		}
		c = m
	}

	for i := 0; i < dataLength; i++ {
		result = append(result, data[i]&0xff, uRightShift(data[i], 8)&0xff, uRightShift(data[i], 16)&0xff, uRightShift(data[i], 24)&0xff)
	}

	if sizeOnLast {
		return append(result, int32(c))
	} else {
		return result
	}
}

// 在go中实现无符号右移（>>>）
func uRightShift(number int32, shift int) int32 {
	return int32(uint32(number) >> shift)
}

func get(data []int32, index int, length int) int32 {
	if index >= length {
		return 0
	} else {
		return data[index]
	}
}

func int32ToAsciiBytes(data []int32) []byte {
	result := make([]byte, len(data))
	for i, number := range data {
		result[i] = byte(number)
	}

	return result
}

```
</details>

更详细的加密算法分析过程请看[这里](https://www.ciduid.top/2022/1113/srun-login-analyze/)
