好久没折腾了,这几天刚开学有点闲下来了,探索一下杭电助手的健康打卡接口,杭电助手也算是很不错的技术社团。技术栈都还算前沿。

一开搞发现还挺有难度的,也是第一次自己独自尝试找前端加密规则(搞完发现还挺简单的),主要被微信内置浏览器抓包和 sign 参数绊了一会儿,前前后后花了大概两个半小时。

打卡接口

https://api.hduhelp.com/base/healthcheckin

请求方式:POST

Header参数:

参数名类型内容备注
authorizationstringtoken <TOKEN>OAuth2.0 认证。注意中间空格。<TOKEN>获取通过 HDU 统一认证平台获取。

token 可以通过访问 https://healthcheckin.hduhelp.com 抓包手动获取,有效期一个月。

自动登录 hdu-cas 获取 token 请看 https://blog.ljyngup.com/archives/874.html/

Query参数:

参数名类型内容备注
signstring校验参数SHA1 算法40位不加盐,签名错误返回 418 状态码。具体计算方式如下
// 字符串形式相连接
// <NAME> 姓名,与Body中保持一致
// <ID> 学号
// <TIMESTAMP> 10位时间戳,与Body中保持一致
// <PROVINCE> 省份编号,与Body中保持一致
// <CITY> 城市编号,与Body中保持一致
// <COUNTRY> 区编号,与Body中保持一致
//
// 伪代码 GO实现: https://gist.github.com/iyear/5e0715c7d869414e04879d0e259b9825
SHA1(<NAME> + BASE64(<ID>) + <TIMESTAMP> + BASE64(<PROVINCE>) + <CITY> + <COUNTRY>)

Body参数(application/json):

参数名类型内容备注
namestring姓名-
timestampnum时间戳10位长度(UTC时间),经测试只用于 sign 计算,实际打卡时间由后端生成并写入
provincestring省份编号-
citystring城市编号-
countrystring区编号-
answerJsonStrstring打卡提交数据JSON文本表示,注意转义

https://healthcheckin.hduhelp.com/js/app.8d49ea3c.js 中搜索 t.default 即可找到编号数据

以实际参数为例对 answerJsonStr 说明:

ques 是答案数据,应该是历史原因看着设置得挺乱的,能抓包就抓包拿来当数据,不行的话就老老实实去看 app.js 好了。

carTo 是编号数据的字符串数组,就是提交的省份、城市、区编号。

{
    "ques1": "健康良好",
    "ques2": "正常在校(未经学校审批,不得提前返校)",
    "ques3": null,
    "ques4": "否",
    "ques5": "否",
    "ques6": "",
    "ques7": null,
    "ques77": null,
    "ques8": null,
    "ques88": null,
    "ques9": null,
    "ques10": null,
    "ques11": null,
    "ques12": null,
    "ques13": null,
    "ques14": null,
    "ques15": "否",
    "ques16": "否",
    "ques17": "无新冠肺炎确诊或疑似",
    "ques18": "37度以下",
    "ques19": null,
    "ques20": "绿码",
    "ques21": "否",
    "ques22": "否",
    "ques23": "否",
    "ques24": "共二针 - 已完成第二针",
    "carTo": [
        "330000",
        "330100",
        "330104"
    ]
}

请求示例 (已脱敏) :

curl 'https://api.hduhelp.com/base/healthcheckin?sign=47944bac930a19d59d5218b8d45a32bda327e5c1' \
  -H 'authorization: token dxxxxxxx-1axx-47xx-aaxx-dxxxx2dxxxx1' \
  -H 'content-type: application/json;charset=UTF-8' \
  -H 'referer: https://healthcheckin.hduhelp.com/' \
  --data-raw '{"name":"xxx","timestamp":1631237051,"province":"330000","city":"330100","country":"330104","answerJsonStr":"{\"ques1\":\"健康良好\",\"ques2\":\"正常在校(未经学校审批,不得提前返校)\",\"ques3\":null,\"ques4\":\"否\",\"ques5\":\"否\",\"ques6\":\"\",\"ques7\":null,\"ques77\":null,\"ques8\":null,\"ques88\":null,\"ques9\":null,\"ques10\":null,\"ques11\":null,\"ques12\":null,\"ques13\":null,\"ques14\":null,\"ques15\":\"否\",\"ques16\":\"否\",\"ques17\":\"无新冠肺炎确诊或疑似\",\"ques18\":\"37度以下\",\"ques19\":null,\"ques20\":\"绿码\",\"ques21\":\"否\",\"ques22\":\"否\",\"ques23\":\"否\",\"ques24\":\"共二针 - 已完成第二针\",\"carTo\":[\"330000\",\"330100\",\"330104\"]}"}'

打卡成功响应:

{
    "cache": false,
    "data": {
        "code": 200,
        "data": "",
        "message": "保存成功!"
    },
    "error": 0,
    "msg": "success"
}

重复打卡响应 (已脱敏) :

{
  "cache": false,
  "data": {
    "code": 200,
    "data": {
      "answerJsonStr": "\"{\"ques1\":\"健康良好\"...",
      "city": "330100",
      "country": "330104",
      "creator": "2105xxxx",
      "gmtCreate": {
        "date": 11,
        "day": 6,
        "hours": 8,
        "minutes": 19,
        "month": 8,
        "nanos": 0,
        "seconds": 39,
        "time": 16313000000,
        "timezoneOffset": -480,
        "year": 121
      },
      "gmtModified": null,
      "id": 9660000,
      "idType": "1",
      "modifier": "",
      "name": "xxx",
      "province": "330000",
      "reportTime": "2021-09-11 08:xx:xx",
      "schoolId": "2105xxxx2021-09-11",
      "schoolno": "2105xxxx",
      "xueyuan": "计算机学院(软件学院)"
    },
    "message": "今日已填报!"
  },
  "error": 0,
  "msg": "success"
}

探索过程

电脑打开 https://healthcheckin.hduhelp.com/ 发现没有打卡按钮,应该是没有微信授权 Cookie 就不显示按钮了 ,抓不了提交的包,于是找办法抓包微信内置浏览器。

尝试了 Fiddler 抓包、 替换电脑微信浏览器内核调出 dev tools,均失败。

最终找到了一个最佳方法,使用感受和调试网页一模一样——通过 Chromeinspect 功能对 Chromium内核浏览器进行远程调试。

方法在 https://momane.com/debug-webpage-on-mobile-and-wechat-in-chrome 中,如果还无法调试可以在 https://debugx5.qq.com信息 栏打开所有和调试有关的开关,再回来用 Chrome 调试。

解决了调试问题,那么就提交抓个包,很轻松抓到了提交数据的包。

1

浏览一下提交的数据,基本都是明文,只有一个 sign 需要我们研究,反正前端的签名肯定是可以找出来的,只是时间问题。

下载 webpack 打包的 app.js ,第一次看混淆丑化的代码,不太熟练,格式化代码后搜索 sign ,总共 11 个结果,筛选后发现以下代码块

2

可以看见 sign 上方就是我们刚刚抓到包的数据,可以断定这个就是我们要的 sign 。整个加密就 be 是未知的算法,其他都是内置函数。

跳转到 be 定义,发现 be = a("6199") ,结合文件开头一堆的不明数字和 .exports ,盲猜是函数出入口。

全文搜索 6199 结果只发现了另一个同样用 a 来做出入口的 W = a("6199") ,然后就卡死在这里,一直在看前面的函数导出导入,一堆 a e t 看晕了。看了半天才想起来另一个 chunk-vendors.js ,进入搜索 6199 ,只有一个结果。

3

结合下面的代码关键词和这块注释,推断调用的第三方 SHA1 库,没有什么复杂逻辑在里面。

尝试不加盐 SHA1 计算得出的字符串,完美符合,至此分析结束。

第二天测试接口的时候忘记改时间戳直接提交了,后来改了时间戳想着覆盖一下,发现重复打卡响应里的时间戳是今天的,那么提交数据的时间戳应该就是校验用途了

时隔一天的时间戳都能正常提交,就压根没做时间戳范围校验,可以说这个 sign 参数也是废了,抓过一次包就可以拿着这个包重复提交,是个挺严重的问题

题外话

发现杭电助手请求很快,顺手查了一下 hduhelp.com 的信息

服务器在杭州阿里云,应该是备案过的。

WHOIS查出域名注册在 2013年10月。

再查备案信息,主体为 廖凌云 ,找到 https://www.hdu.edu.cn/news/important_27528;https://www.hdu.edu.cn/news/media_23517 两条新闻。

不知这位学长现在是否还在杭电助手一线😂

其他接口

其他接口不用微信内置浏览器就可以抓,而且数据基本都是明文,简略写一下。

所有提交只需要一个OAuth认证头

Header参数:

参数名类型内容备注
authorizationstringtoken <TOKEN>OAuth2.0 认证。注意中间空格。<TOKEN>获取通过 HDU 统一认证平台获取

Validate

https://api.hduhelp.com/token/validate

获取认证信息

请求方式:GET

响应示例 (已脱敏) :

// 可以看出是一个有着历史包袱的接口
{
    "Data": {
        "accessToken": "06xxxxxx-bxxx-4xxx-9xxx-f34xxx78xxxa",
        "clientID": "healthcheckin",
        "expiredTime": 1633780760, // token过期时间
        "grantType": "",
        "isValid": 1,
        "isViald": 1,
        "school": "hdu",
        "staffId": "2105xxxx", // 学号
        "tokenType": 0,
        "uid": 4600000 // 杭电助手内的uid
    },
    "cache": false,
    "data": {
        "accessToken": "06xxxxxx-bxxx-4xxx-9xxx-f34xxx78xxxa",
        "clientID": "healthcheckin",
        "expiredTime": 1633780760,
        "grantType": "",
        "isValid": 1,
        "isViald": 1,
        "school": "hdu",
        "staffId": "2105xxxx",
        "tokenType": 0,
        "uid": 4600000
    },
    "error": 0,
    "msg": "success"
}

Info

https://api.hduhelp.com/salmon_base/person/info

获取个人信息

请求方式:GET

响应示例 (已脱敏) :

{
    "cache": true,
    "data": {
        "staffId": "2105xxxx", // 学号
        "staffName": "xxx",    // 姓名
        "staffState": "在校",
        "staffType": "1",
        "unitCode": "05"
    },
    "error": 0,
    "msg": "success"
}

Daily

https://api.hduhelp.com/base/healthcheckin/info/daily

获取今日提交数据

请求方式:GET

响应示例 (已脱敏) :

{
    "cache": false,
    "data": {
        "code": 200,
        "data": {
            "answerJsonStr": "\"{\"ques1\":\"健康良好\"....", // 省略部分信息
            "city": "330100",
            "country": "330104",
            "creator": "2105xxxx",
            "gmtCreate": {
                "date": 11,
                "day": 6,
                "hours": 8,
                "minutes": 19,
                "month": 8,
                "nanos": 0,
                "seconds": 39,
                "time": 1631300009000, // UTC时间,需自己转换时区
                "timezoneOffset": -480,
                "year": 121
            },
            "gmtModified": null,
            "id": 9660000,
            "idType": "1",
            "modifier": "",
            "name": "xxx",
            "province": "330000",
            "reportTime": "2021-09-11 08:00:00",
            "schoolId": "2105xxxx2021-09-11",
            "schoolno": "2105xxxx",
            "xueyuan": "计算机学院(软件学院)"
        },
        "message": "今日已填报!"
    },
    "error": 0,
    "msg": "success"
}

Code

https://api.hduhelp.com/salmon_base/health/code

获取个人健康码信息

请求方式:GET

响应示例 (已脱敏) :

{
    "cache": true,
    "data": {
        "codeStatus": "绿码",
        "lastUpdateTime": "2020-02-20T00:00:00.000+0000",
        "location": "xx市",
        "reason": "你去过疫情关注地区",
        "serverUpdateTime": "2021-09-11 00:00:00"
    },
    "error": 0,
    "msg": "success"
}

History

https://api.hduhelp.com/base/healthcheckin/history

获取历史打卡记录

请求方式:GET

响应示例 (已脱敏) :

{
    "cache": false,
    "data": [
        {
            "Answer": {
                "carTo": [
                    "330000",
                    "330100",
                    "330104"
                ],
                "ques1": "健康良好",
                "ques10": null,
                // ...
            },
            "City": "330100",
            "Country": "330104",
            "IDType": 1,
            "Name": "",
            "Province": "330000",
            "ReportTime": 1631237035, // UTC时间,需自己转换时区
            "SchoolName": "计算机学院(软件学院)",
            "StaffID": "2105xxxx"
        },
        {
            "Answer": {
                "carTo": [
                    "330000",
                    "330100",
                    "330104"
                ],
                "ques1": "健康良好",
                "ques10": null,
                // ...
            },
            "City": "330100",
            "Country": "330104",
            "IDType": 1,
            "Name": "",
            "Province": "330000",
            "ReportTime": 1631150879, // UTC时间,需自己转换时区
            "SchoolName": "计算机学院(软件学院)",
            "StaffID": "2105xxxx"
        }
        // ...
    ],
    "error": 0,
    "msg": "success"
}
最后修改:2021 年 09 月 18 日 07 : 16 PM