叠甲
先叠个甲: 这大抵是没啥问题的吧, 毕竟在我折腾完之后发现网上有不少这样的 srun 登陆项目.
并且本文仅供学习参考, 适用于网络安全系同学 Web+RE+Crypto 方向的学习 (从标题 writeUP 估计你也大概知道这是个什么东西了), 你可以把本文作为一个简单的逆向和网络抓包的教学文章, 所以本文仅供教学使用, 请不要想一些不切实际的邪恶想法.
前置工具与知识
你需要的东西
- 一个支持 HTTPS 抓包的工具 (Charles, Proxyman (我用的是这个的免费版), Wireshark, Fiddler...), 需要注意的是, 因为需要 HTTPS 抓包, 所以需要安装对应的 SSL 证书;
- 一个你愿意就行的编程语言
- 一个闲人
相关的知识
获得源码
访问 portal.ucas.ac.cn, 用开发者工具获得以下文件:
main.js
其中, 根据 CONFIG
(在 html 中定义) 创建了一个 Portal
类的实例.
只关心使用 JQuery $('#login-account')
绑定的函数, 其调用了 Portal.login
的方法, 故定位到 Portal.js
中的 'login'
.
Portal.js
虽然里面的代码做了一些奇奇怪怪的 "混淆", 但是其实核心就是在实现一个类的定义, 找到 key: "login"
, 可以发现其对应的函数调用的是 _loginAccount
这个 private 方法.
其他的方法也可以类似地找出来, 比如当前状态检查, 退出登陆之类的方法, 不过对于目前来说, 暂时只关心当前状态 (对应 /cgi-bin/rad_usr_info
) 与登陆 (对应 /cgi-bin/srun_portal
).
API 的一般调用
定位到 _this.ajax.jsonp(host, url, params)
的函数实现,
其实就是一个 JQuery 的 JSON Object 的回调, 通用的形式如下:
http://portal.ucas.ac.cn/${API_PATH}/?callback=${CALLBACK}&${MORE_PARAMS}
其返回的 content 形式如下:
${CALLBACK}(${JSON_OBJECT})
比如:
callback({"error":"ok"})
很容易就实现类似的 API 调用的通用接口:
(defun api (api &rest queries &key (callback "callback") &allow-other-keys)
"GET SRUN API results as JSON (hash-table).
Return values are error-code (error in JSON Object) and JSON as hash-table. "
(let* ((queries (if (getf queries :callback) queries
(cons :callback (cons callback queries))))
;; 这里将输入的参数变成 URL 的 query
(queries (loop for (key val) on queries by #'cddr
collect (cons (str:snake-case (symbol-name key)) val)))
;; 从预先定义的 API Path 中读取对应名称的 API 的 URL path
(path (or (second (assoc api *api*))
(error (format nil "NO such api for ~A" api))))
;; 请求的 API
(url (quri:make-uri :scheme "https"
:host *host*
:path path
:query queries))
;; 返回值是一个 callback({...}) 形式的东西
(response (dex:get url))
;; 从中提取 JSON 并进行解析
(json (shasht:read-json
(str:substring (1+ (length callback))
(1- (length response))
response))))
(values (alexandria:make-keyword
(str:upcase (str:param-case (gethash "error" json))))
json)))
当前状态
(api :info) ;; => :ok, #<hash-table>
;; 其中 #<hash-table> 中的 "client_ip" 项为当前设备的 IP
退出登陆
(api :logindm
:ip ip
:username username
:time (unix-timestamp) ;; 请自行了解 UNIX timestamp
:unbind "0"
:sign (sha1 (str:concat time username ip unbind time)))
可以发现, logout 竟然是没有密码验证的, 这其实存在一定的风险, 尤其是相对来说 username
几乎就是一种明文, 而 ip
又是一个可爆破的密码的情况下, 但是绝对不是鼓励大家干这种鸟事, 互联网不是法外之地.
登陆
(api :auth
:action "login"
:username username
:password (str:concat "{MD5}" hmd5)
:os device
:platform platform
:double-stack "0"
:chksum (sha1 str)
:info i
:ac-id ac-id
:ip ip
:n "200"
:type "1")
其中 i
为: _encodeUserInfo
的产物; str
为 token
, username
, token
, hmd5
, token
, ac-id
, ip
, n
, type
, i
的字符串拼接.
最难的部分 _encodeUserInfo
由三个函数组成:
s
: 将输入的字符串变成 4 个一组的比特数组;
l
: 将比特数组还原成字符串;
encode
: 功能并不太理解, 据密码佬说, 应该是 XXTEA;
其中在模拟 encode
行为的时候, 发现 JavaScript 的位运算比较坑爹, 因为强制位运算是在 32 位 "带符号" 的数据上进行的, 所以符号位的影响比较大.
不过基本上还行
一些 debug 的小技巧
为了保证你的代码是对的, 你可以通过做中间断点的方式来逐步比较对应的变量在计算过程中是否正确, 从而可以定位具体是哪个表达式的 bug.
后记
可以参考完整代码: Gist, 其中的注释应该写得非常详细了, 甚至我还保留了测试代码呢...