如何优雅地在北邮洗澡

由于疫情原因,校内开始对人流进行限制,为了避免浴室大量聚集,因此需要提前对浴室进行预约。

最早是每 2020 分钟开放 153153 个预约名额。在初期大部队未返校时,这个数目绰绰有余,不需要提前预约。
而当大部队返校后,产生了无数问题。以下午 33 点到晚上 1111 点允许洗澡为例,共有 88 个小时洗澡时间,每小时有三组队伍,每个队伍开放 153153 人,也即一天能洗澡的人数为 3×8×153=36723 \times 8 \times 153 = 3672。而后来每个时间段允许预约人数提升为 260260 人,一天能洗澡的人数为 3×8×260=62403 \times 8 \times 260 = 6240
而很不巧的是,根据“本科毕业生去向”数据显示,每一届的毕业人数都高达 30003000 人,也即澡堂只能供给两个年级洗澡,尽管并非所有学生都在本部,但考虑到还有大量的研究生、博士生,这个数量无法保证每个人每天都能洗澡。

那么如何优雅地在北邮洗澡呢(不考虑别人能不能洗得上)?

Chrome 调试微信页面

由于很多微信内部的网页依赖于微信本身的内容,因此无法在 Chrome 打开并调试,只会显示“请在微信客户端打开链接”的提示

但是,这并不意味着不能调试。首先要看这些页面为什么要求必须通过微信打开:

  1. 页面没有做 PC 端适配,只针对微信浏览器做了适配
  2. 页面需要获取微信用户信息

修改 UserAgent

对于前者,如果要实现该功能,基本的思路是判断 UserAgent 是否为MicroMessenger

当然,如果更严格的浏览器,也可能会判断是否存在微信浏览器私有的 JS 函数。而巧的是,微信还真有私有函数:WeixinJSBridge,其主要功能为:

  1. 分享给好友
  2. 分享到朋友圈
  3. 分享到微博
  4. 隐藏下方工具栏
  5. 隐藏微信右上角分享按钮(三个小圆点)
  6. 关闭浏览器回到公众号对话窗口

但是考虑到大部分页面实际上不存在这些功能,因此大概率不会采用这种方式判断。

因此,只需要修改 UserAgent,原理上就可以跳过部分页面的验证。

要修改 UserAgent,可以在 Chrome 开发者工具的 More tools(右上角的三个圆点的下拉菜单中)下的 Network condition 中进行设置
将 select automatically 取消勾选,并且在下面的 custom user agent 中填写MicroMessenger

在不关闭开发者工具的前提下,该开发者工具对应页面的 UserAgent 就会使用MicroMessenger(这也意味着,不能使用在浏览器打开来进入页面,而应该复制页面的地址而后粘贴到对应的 Chrome 标签中)

获取微信 Cookies

如果页面需要获取当前微信用户的信息,那么即使修改了 UserAgent,仍然没有作用,因为浏览器不知道你是谁。

微信开发平台中,用户身份的验证使用 Auth2.0。但不管是何种验证流程,一般而言,服务端都会对验证结果进行缓存,重新为用户颁发一个 token 作为身份令牌。

这么做的目的主要是尽可能减小验证请求的次数,尽管对方是微信,服务相对稳定,但是也没有必要用户每次刷新都重新进行验证,这将消耗大量不必要的时间。

因此,如果可以获取 token,那么在浏览器中服务器也能获取我们的身份。但是如何获取微信浏览器的 token 呢?

通常而言,token 会出现在以下位置中:

  • cookies
  • LocalStorage
  • SessionStorage
  • Header
  • 接口请求中

无论是哪种 token 机制,token 必然要存储在一个浏览器本地,否则每次刷新都需要重新鉴权。通常,最佳的存储位置是 cookies 和 LocalStorage。严格意义上而言,除去空间大小外,在这里 token 存储在哪都没有区别,只是 cookies 兼容性会更高一点。当然,token 只是一个很短的字符串,因此存在哪里都没区别,往往大家会将其存放在 cookies 中。

如何获取 cookies 呢?最简单的办法是抓包,使用任意一个抓包工具,抓取微信浏览器的包。需要额外注意的是,如果是 https 页面,可能需要额外的配置,否则只能抓到加密后的内容。在这里,使用 Fiddler 作为抓包工具。

由于微信本身就有电脑端,因此在选项中开启抓取 HTTPS 后,即可直接开始抓包。如果想要抓取手机流量,还需要在 Connections 选项卡中允许远程设备连接,而后配置手机和电脑在同一局域网下,并设置代理为 Fiddler 的的端口(默认为8888)

如果没有意外的话,在微信内访问对应的页面后,可以看到相应的访问内容。在点击对应的项目后,可以看到 cookies 的数值。
从协议上而言,cookies 是一个长字符串,但是按照特定格式进行分割,其在逻辑上是一个 key-value 字典。只需要将这个 cookies 设置到 Chrome 中即可。

在 Chrome 中设置 cookies 推荐使用 EditThisCookie 插件,一般来说 cookies 需要设置与原本微信中的对应域名、过期时间一致。其中,对应域名极为重要,将会直接影响服务器能否成功取得 cookies(理论上应该是.qq.com

成功设置 cookies 后,重新粘贴页面地址,应该就可以成功访问页面了(实际上在身份验证失败后,会跳转到微信的 Auth2.0 验证接口,因此需要重新返回之前的页面)

北邮浴室预约系统

页面逻辑

从页面上来看,这是一个极为“简陋”的前端页面:

  • 服务端渲染
  • 使用 form 发送信息
  • 没有过多使用 js

(当然严格来说这其实是优点,会极大提高加载速度)

实际上即使不查看页面内容,单靠点击预约后的抓包也可以获取预约的接口。这是一个简单的 post 接口,由于直接使用 form 请求,因此基本上不需要考虑额外的模拟,携带 cookies 和 UserAgent 即可,预约的参数也是明文传输。

在 Python 中使用 requests 库设定 cookies 和 UserAgent 即可

接口研究

尽管上述内容已经完成了预约功能,但是还是有必要研究下这个接口本身。

首先,可以看到该接口返回的其实是一个页面,也即请求成功后要显示的整个页面内容。可以从其中的警告部分获取请求的状态:

  • 预约成功
  • 重复预约
  • 人数已满

同时,页面内部还存在学号,可以用于判断 token 是否失效。

那么剩下就是对接口的安全性进行研究。首先,对着一个人数已满的时间段进行预约,不出所望返回人数已满的提示。重复预约同样。这说明所有的预约判定都是从后端进行验证的,非常合理。

但是如果预约不合法的时间段呢?比如预约一个不存在的时间段,或是提前预约很久后的时间段。
页面将会返回 400 状态的报错页面。这个页面详细输出了调用堆栈、数据库查询……简单来说就是该服务以开发模式部署于服务器,虽然没有泄露特别敏感的内容,但是仍然存在着极大的安全隐患。

如何防止自动预约?

尽管最初研究的目的是研究如何自动预约,但是实际上更深一步研究如何防止自动预约更有意义。

  • 从前面提到的思路来看,如果可以,应该使用 WeixinJSBridge 来判断是否是微信浏览器。尽管意义不大,但是可以增加分析的难度;
  • 北邮浴室预约使用的是 HTTP 协议,虽然 HTTPS 抓包也只是增加了部分难度,但是使用更为安全的协议仍然具有重大意义;
  • 接口参数应该加密传输,至少应该有一个校验的哈希值;
  • 在生产环境下使用生产模式,尽管目前没有发现问题,但是存在直接攻入数据库的可能性;
  • 最直接的应该是针对不合法的请求,直接封禁账号:
    • 在前端严谨的情况下,任何请求都应该在一个合法的范围内,如果出现不合法的请求可以近似认为存在攻击
    • 不合法包括参数不合法、请求间隔不合法……
    • 这将极大增加脚本测试过程中试错难度

严格来说,前端没有秘密,即使全程加密,也总能找到模拟请求的手段。所以最直接的解决办法是从行政上解决问题,而非从技术上。(不过浴室开放到 260260 已经算是满负荷了,大概只有再建一个浴室才能解决问题,否则也只是不会有自动预约脚本,浴室仍然爆满)