wust武科大校园网深澜srun认证api分析(后续)
发表于 [2022-11-13 20:22:00],更新于 [2023-06-29 22:11:99] 总字数 [4.5k], 阅读时长 [约21分钟], 阅读量 [
未知
] publish by lensfrex
上回书说到,彼时的璃月….
书接上回:某校园网认证api分析
上一次对校园网web认证做了个简单的请求分析,但是一些关键的字段是用了某种神奇的算法进行了加密发送,这里就对这个算法进行简单的分析,并且用go语言重新实现一遍
(说起来好像从7月初就一直鸽到了现在了吧…嘘🤫)
抓包分析 干这种事情首先要做的就是抓包,最简单的就是直接浏览器F12,完整地过一次登录请求
1. 请求challenge码 最先发出来的是一个GET请求:
http://59.68.177.183/cgi-bin/get_challenge?callback=jQuery1124005588867363182781_1668219915986&username=202100000000&ip=10.16.1.9&_=1668219915990
请求参数:
1 2 3 4 callback: jQuery1124005588867363182781_1668219915986 username: 202100000000 ip: 10.16.1.9 _: 1668219915990
这里的callback一定要设置,否则返回的是杂乱的数据(没有json结构),只有一个ok,是没有咱们想要的challenge码的
其他字段顾名思义,ip就是当前连接之后获取到的ip,下划线字段是当前时间戳(毫秒),注意这个时间是GMT时间,而不是咱们的东八区时间
获取到的响应如下:
1 2 3 4 5 6 7 8 9 10 11 12 jQuery1124005588867363182781_1668219915986({ "challenge" : "3c6d08d667d0ee0ccad77c55b19d3e4ab2552f7163ec40a9389095a18f86c398" , "client_ip" : "10.16.1.9" , "ecode" : 0 , "error" : "ok" , "error_msg" : "" , "expire" : "52" , "online_ip" : "10.16.1.9" , "res" : "ok" , "srun_ver" : "SRunCGIAuthIntfSvr V1.18 B20211105" , "st" : 1668219964 } )
这里我们只需要关心challenge字段的值就好了,后面有用。
2. 请求认证 继续分析,接下来的也是一个GET请求:
http://59.68.177.183/cgi-bin/srun_portal?callback=jQuery1124005588867363182781_1668219915986&action=login&username=202100000000&password=%7BMD5%7Dr096b1282e1a50ce8f9d15aa3a29acf8&os=Linux&name=Linux&double_stack=0&chksum=dfa1459124878b981873bd6d853c88b1eb716e6d&info=%7BSRBX1%7D76z3vHCupat5bbo3et2MbNplTCn0FXWKd%2FhazIzb26HpqJsIoolCvtcLPk5mnstlz0J%2BebaMjGfckCzVHlaUqqGaAJ7XM%2FEQ83p0D2TPbjDG7f%2FiFEiadcQkHJpiwOBr800LDP7yrA4%3D&ac_id=7&ip=10.16.1.9&n=200&type=1&_=1668219915991
请求参数(部分字段经过修改,请以实际为准):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 callback: jQuery1124005588867363182781_1668219915986 action: login username: 202100000000 password: {MD5}r096b1282e1a50ce8f9d15aa3a29acf8 os: Linux name: Linux double_stack: 0 chksum: dfa1459124878b981873bd6d853c88b1eb716e6d info: {SRBX1}76z3vHCupat5bbo3et2MbNplTCn0FXWKd/hazIzs26HpqJsIoolCvtcLPd5mnstlz0J+ebaMjGfckCzVHlaUqqGaAJ7XM/EQ83p0D2TPbsDG7f/iFEiadcQkHJpiwOBr8002DP7yrA4= ac_id: 7 ip: 10.16.1.9 n: 200 type: 1 _: 1668219915991
翻看js源码,不难发现大部分字段其实是写死了的(在下一节会详细讲),动态的字段也比较好算出来,除了info字段算法比较特殊外,其他的都是普通的算法,翻翻js源码就知道了
这个请求完成以后,不出意外的话,就能上网了
这个请求的响应如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 jQuery1124005588867363182781_1668219915986({ "ServerFlag" : 0 , "ServicesIntfServerIP" : "172.26.23.173" , "ServicesIntfServerPort" : "8001" , "access_token" : "3c6d08d667d0ee0ccad77c55b19d3e4ab2552f7163ec40a9389095a18f86c398" , "checkout_date" : 0 , "client_ip" : "10.16.1.9" , "ecode" : 0 , "error" : "ok" , "error_msg" : "" , "online_ip" : "10.16.1.9" , "ploy_msg" : "E0000: Login is successful." , "real_name" : "" , "remain_flux" : 0 , "remain_times" : 0 , "res" : "ok" , "srun_ver" : "SRunCGIAuthIntfSvr V1.18 B20211105" , "suc_msg" : "ip_already_online_error" , "sysver" : "1.01.20211105" , "username" : "202100000000" , "wallet_balance" : 0 } )
这下认证的请求部分就结束了
刨 看完了请求是怎么请求的,现在就来看看这些请求的数据是怎么个算出来的吧
这个认证页面主要的业务逻辑主要是放在了Portal.js
这个js脚本里
这个js脚本简直是好得不得了,只有简单的混淆(或者说…其实根本就没混淆),而且还有详细的注释,真的是太良心了
简单找找,能够发现负责请求的函数就是这段(大约在995行附近):
小心,有巨量代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 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 ] : '' ; var ip = _this.portalInfo .doub && host ? '' : _this.userInfo .ip ; _classPrivateFieldGet (_assertThisInitialized (_this), _getToken).call (_assertThisInitialized (_this), host, ip, function (token ) { 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; 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 , 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 ; 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 ; if (res.ecode === 'E2620' && CREATER .useOnlineDeviceMgr ) return _this.confirm ({ message : _this.translate ('E2620Tips' ), confirm : function confirm ( ) { _this.dialog .open ('onlineDeviceMgr' , function ( ) { _this.getOnlineDevice (); }); } }); 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) { pendingReqNum -= 1 ; } }); }; sendAuth (); if (_this.portalInfo .doub ) { var _this$portalInfo3 = _this.portalInfo , ipv4 = _this$portalInfo3.ipv4 , ipv6 = _this$portalInfo3.ipv6 ; sendAuth (_this.portalInfo .nowType === 'ipv4' ? "[" .concat (ipv6, "]" ) : ipv4); } var timer = setInterval (function ( ) { if (pendingReqNum <= 0 && _this.online ) { clearInterval (timer); if (obj.success ) obj.success (successMsg); if (!obj.success ) _this.toSuccess (); } if (pendingReqNum <= 0 && !_this.online ) { clearInterval (timer); } }, 500 ); setTimeout (function ( ) { return pendingReqNum = 0 ; }, 3000 ); }
核心的是这段:
展开查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 var sendAuth = function sendAuth ( ) {var host = arguments .length > 0 && arguments [0 ] !== undefined ? arguments [0 ] : '' ;var ip = _this.portalInfo .doub && host ? '' : _this.userInfo .ip ; _classPrivateFieldGet (_assertThisInitialized (_this), _getToken).call (_assertThisInitialized (_this), host, ip, function (token ) { 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; 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 , double_stack : _this.portalInfo .doub && !host ? 1 : 0 , chksum : sha1 (str), info : i, ac_id : ac_id, ip : ip, n : n, type : type },
第一个请求的格式很好理解,这里就不多分析了,咱先来看看第二个请求的代码
通过第二个请求的参数分析,不难发现这些字段来自这里:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 action : 'login' ,username : username,password : _this.userInfo .otp ? '{OTP}' + password : '{MD5}' + hmd5,os : _this.portalInfo .userDevice .device ,name : _this.portalInfo .userDevice .platform ,double_stack : _this.portalInfo .doub && !host ? 1 : 0 ,chksum : sha1 (str),info : i,ac_id : ac_id,ip : ip,n : n,type : type
上下文找找,这里的action
, os
, name
, n
等等都是固定的,推测的含义大概如下:
字段
含义
action
操作,这里当然就是login
username
用户名,就是学号
password
密码,在这里就很容易发现其实就是md5(密码原文+challenge)
os
操作系统
name
操作系统名称,在这个js文件里边都能找到定义
double_stack
看注释就知道是否为双栈(ipv4/6)认证(但是不知道为什么没有ipv6…)
chksum
参数校验,值为str字段进行sha1计算之后的值了
info
一些登录信息,具体算法比较特殊,下面再仔细讲讲
ac_id
ac id
n
不是很清楚,但是看上面的代码是写死固定的200
type
类型,不知道是什么的类型,看上面写的也是写死的1
info字段加密分析 不难看出,对i赋值的是这段代码
1 2 3 4 5 6 7 var i = _classPrivateFieldGet (_assertThisInitialized (_this), _encodeUserInfo).call (_assertThisInitialized (_this), {username : username,password : password,ip : ip,acid : ac_id,enc_ver : enc}, token);
这些函数都只是虚晃一枪,都只是一些回调之类的操作,真正执行函数的是_encodeUserInfo
函数,call里边异步回调的时候把一个js对象和token(也就是上面拿到的challenge码)传到了_encodeUserInfo
函数里边。
那这个函数究竟在哪?
不用太复杂的方法,直接粗暴Ctrl+F找就行了,找一找,原来是在这里:(约681行处)
展开查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 _encodeUserInfo.set (_assertThisInitialized (_this), { writable : true ,value : function value (info, token ) {var base64 = _this.clone ($.base64); base64.setAlpha ('LVoJPiCN2R8G90yg+hmFHuacZ1OWMnrsSTXkYpUq/3dlbfKwv6xztjI7DeBE45QA' ); 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));} });
好!接下来就是改写了。
一般来说,如果过于复杂的话咱们通常是直接调用js执行,但是嘛…这就可能会把咱们的程序搞得非常庞大,效率也很低,但是也不是不能用是吧…
这段看起来不是很难,那咱就开工
改造,开工! 粗略看下来,主要就是用encode函数生成了一个字符串。再对这个字符串进行一次base64编码(当然,是被动过手脚的)
就是这里被动了手脚:base64.setAlpha('LVoJPiCN2R8G90yg+hmFHuacZ1OWMnrsSTXkYpUq/3dlbfKwv6xztjI7DeBE45QA');
虽然说是动了手脚,但是具体的base64算法是一样的,只不过是字母表被换了顺序而已
encode函数在开头调用了两次s函数,把str和key转成了又一个神奇的数组,这里的str就是前面传进来的,转成了json字符串的js对象
那咱们就先改造s函数。
s函数
1 2 3 4 5 6 7 8 9 10 11 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; }
这个简单,就是一些普通的位移操作而已,直接照着葫芦画瓢就行了
咱就用go写一写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 }
因为js里边数组越界也是不会报错的,但是go和大部分语言就不一样了,所以咱们要另外新开一个函数简单的get函数出来,数组越界了就直接返回0而不是报错
1 2 3 4 5 6 7 func get (data []int32 , index int , length int ) int32 { if index >= length { return 0 } else { return data[index] } }
然后就是l函数了:
l函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ('' ); }
这个也简单,跟上面的一样照着来就好了,但是有一些问题还是需要注意的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 } }
可以看到,这里又有一个新的函数uRightShift
1 2 3 4 func uRightShift (number int32 , shift int ) int32 { return int32 (uint32 (number) >> shift) }
其实就是无符号右移>>>在go中的实现
接下来就是重头戏encode
函数了:
encode函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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 ); }
改写成Go之后是这个样子的,一些常数在这里就直接算出来了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 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 ) }
看着挺吓人,但其实还好,照着思路抄下来就好了,最主要的还是强弱类型语言的不同地方要注意一下
最后,这个请求加密就改写好了。完整的代码已开源在Github上:lensferno/canti
具体的加密部分的文件在app/codecs/encode.go 中
就这样啦
咚咚咚折腾一番,发现其实之前就有人研究过了:校园网模拟登录 | xia0ji233’s blog 等等,这个只是其中一个
这里也有另外一个大佬写的go版本,算法可能会更好,这里的只是照着js改而已
Debuffxb/srun-go - main.go