Skip to content

北京考试院是如何把考生数据泄漏的风险提高到 101% 的?


北京教育考试院,简称北京教考院,或北京考试院。以下简称为考试院的单位均为北京教育考试院。

2025 年 12 月 13 日举行高考第一次英语听力口语考试。考试院提前五天开通了准考证下载通道。

2 月 8 日初日漏洞

笔者学校 announce 了准考证开放下载的事情,不过当时也直接告知学校将在考试前一天下发准考证,考生无需自己下载打印。应该是考试院直接把学校单位集体报名的考生的准考证文件打包下发到单位,然后学校单位打印完毕分拣后发放到考生。

当天笔者登录考试院官网查询并下载了准考证。登录下载入口为一个双因素(身份证号+高考报名号,也可能是姓名+报名号,因为当时并未截图,截止到文章修订完成的时间,具体哪种方法已无从考证)验证+验证码的形式,登录后页面便返回了笔者的考试场次(详细时间)和考试单位,以及准考证的下载按钮。

点击下载按钮,跳转到了以下请求:

http://gkjkzkz.bjeea.cn:8080/examinee/downloadAdmission?examineeId=1********

这里 examineId 为 11 位数字,请求得到的就是此数字为名称的 pdf 文件,文件内准考证号和这个 examineeId 是一致的。

也就是说,教育考试院此时的逻辑是,通过双因素验证考生身份,然后开放考生下载准考证。

看起来还挺靠谱的?如果 examineId 是什么神秘 hash,貌似还挺安全的。

但是是这样的吗?

笔者将 examineId 请求的准考证号修改了几位,成功下载到

  • 同考场其他座位号的
  • 其他考场同座位号的
  • 其他场次同考场其他座位号的
  • 本城区其他考点的
  • 其他城区的

各种准考证 pdf。

抽盲盒呢搁这?笔者释怀的笑了。

🤣👉🤡

很遗憾,前文已提到,考试院的 examineId 并不是什么 hash,而是有着具体含义的 11 位数字,而且自增规律非常明显。其数字由 5 部分组成。

1 AABB CC DD EE
  • 1 Magic,占位符
  • AABB 考点号(亦被称为考点代码)
  • CC 场次号
  • DD 考场号
  • EE 座位号

以上全部数字遵循自 01 开始的自增规律。

考点号的自增有特殊性,无法请求到 03XX04XX 的考点的考生信息,而 05XX 刚好是朝阳区的考生。

崇文 & 宣武:还有我的事?

笔者一开始将 AABB 拆分为 AABB 两部分,AA 为城区编码(如 01 为东城区),BB 为考点号。当天晚些时候,学校又公布了一组表格,其中包含笔者所在城区的几个学校的考点名称,考点地址,以及最重要的,考点代码,由四位数字组成,即 AABB 对应出具体的某区某中学,所以合理推测,考试院层面应该就是将两部分数字组合为一个四位数的考点代码。

笔者根据以上规律制作了一个爬虫来下载准考证 pdf。因为考试院未公布考点详情,各个城区的考点,各个考点的考场和座位号具体自增到哪里均未知,所以只能通过剪枝策略尽量嗅探。

2 月 9 日,笔者成功批量下载了东城区 1777 和西城区 3531,共计 5308 份准考证 pdf。

限制于不清楚考场具体自增数据和本人拙劣的编程水平,显然这两个数字都显著低于两城区的实际高考报名人数。

2 月 10 日第一次修复

10 日晚间,笔者想继续下载其他区的准考证,发现原先的 examineId 请求似乎失效,使用准考证号无法再直接下载到 pdf 文件。

笔者登录考试院网站并重新查询下载准考证号,请求接口未发生改变,依然是 examineeId,但是由原本的十一位数字变成了一个 hash。

完结撒花!考试院换成 hash,这下没办法遍历爬虫了!

吧?

如果当时读者你是考试院的开发,看到有人爬虫了几千 pdf,你会如何修改系统呢?

删除掉自增规律显然是一个优秀的解决方案,比如由使用准考证号更换为考生姓名或身份证号。

这其中的原理是非对称。准考证号的特点是有规律性且可被预测的。脚本小子可以猜测出准考证号并且遍历,此时便是对称的数据结构,虽然脚本小子不知道有多少数据,但是只要遍历的足够多,终能将所有的数据遍历出来。

更换为姓名或身份证号很好得将数据非对称化,脚本小子显然是没有区级以上的高考报名名单和身份证号的,但是考试院有啊,脚本小子除非能有考生名单,不然没办法遍历完整个库。

当然其实也可以理解为增加遍历的成本,没准有神人说身份证号这种本身就有一定自增规律的数字,以及姓名的排列组合,也并不是不能遍历。只不过遍历的成本无限增高,命中概率无限接近于 0。

回到正题,考试院的 hash,确实是一招好办法,毕竟脚本小子没有准考证号的 hash,遍历英文字母和数字的排列,那成本不是直接升天了?

你再读一下上面这句话呢?

什么叫准考证号的 hash?

可能就是直觉,笔者当日猜测考试院修改了的请求,使用的 hash 就是准考证号的 hash。

因为笔者并不擅长密码学相关知识,所以请教 Gemini 准考证号和 hash 的相关性,还真就让 Gemini 找到了计算方法,具体方法如下:

  • 将准考证号 11 位数字进行 MD5 hash
  • 转换到 16 进制(此时字符长度 32 位)
  • 转换为 Raw Bytes(字符长度 128 位,16 个字节)
  • Raw Base64 编码

🤣👉🤡

被考试院气笑 (2/1)

看来考试院还是不想加入鉴权,聪明的考试院想到了要非对称化,结果却整了个自增数组 hash 就自认为完备了,和原先对称可遍历的区别在哪?本地加个计算不照样遍历吗?

当然笔者强如菜鸡的编程水平再次发力,没写好请求,忘了是 Raw Base64,没删填充符号,11 日早,笔者给考试院虚空送了上千个 404 请求,当时着急上课,于是计划下午继续下载剩余的准考证。

2 月 11 日紧急下线 & 修复健全

时不我待,终究考试院还是没有给笔者我这个统计全市高考报名详情的机会。

11 日晚考试院下线登录下载准考证的入口,一同关闭了请求。电话查询考试院,说是正在修复。

大概距离观测到入口关闭到入口重新开放,用时一个小时。重新登录查询下载准考证,这一次再点击下载,终于不是那个糖的不行的 GET 请求了,准考证文件变成了 blob 数据,并且最重要的是可以看到请求加入了 cookie,加入了过期时间控制。

至此,考试院终于修复了这个神人鉴权漏洞。

反思,也可以说是笑点解析

不管怎样,终究是有 5000 多份 pdf 被我保存了下来。

从这个考试院的下载鉴权漏洞来看,无法想象考试院这个应当相当权威的政府机构,内部系统的草台程度。

用失责来形容考试院一点也不为过。

不清楚考试院的这个下载系统的鉴权漏洞存在多长时间了,10 号的小补丁到 11 号引入鉴权,考试院本身的网络技术部门显然有能力将鉴权一事做好,但是他们偏要留后门,然后在漏洞被利用了之后采取消极应对,修两次才彻底解决问题。虽然能力肯定是有问题的,但是对待问题的态度确实不是一般的差。不然怎么可能对高考这种国家考试的权威系统搭建也能犯糖到这种程度?

笔者明白肯定也有一大批读者读到此处时一定会过来质问读者,肯定要说什么君子协定,防君子不防小人,笔者这不妥妥做实非法获取计算机信息系统数据、非法控制计算机信息系统罪吗?

是这样没错。但是笔者我觉得笑点解析也就是在这里具象了;

考试院的这个无鉴权漏洞唐氏水平就像是一个厕所,但是上面没说明是公共厕所,但是内部和公共厕所一模一样。笔者上完厕所出来,才发现自己误闯天家,非法入侵。这个漏洞已经不像是漏洞,因为就没有哪里不漏的,说是一个加密系统,一边用 http,一边用堪比公共 API 的请求方式,真的就会让人产生「该不会其实本来就是开放查询考生详情」的「错觉」。

笔者认为这个漏洞的危害性远不止于此。准考证内含高考报名号、考生名称、报名单位本身。结合笔者经历,大部分学校其实在诸如运动会、疫苗接种之类的年纪统计时,基本都有在年纪或校级公布(其实就应该说是泄漏,只不过很多人压根没有觉得这是泄漏)过涵盖了全年级学生姓名、身份证号等关键公民信息的表格。结合姓名、身份证号、报名号以及准考证号本身(虽然不是笔试准考证号),在极早期的北京市考试院系统中,拥有以上数据即可修改考生的报名志愿。不过好在考试院很早就引入了账号系统以及 SMS OTP,除非能劫持到手机验证码,不然志愿报名的防线还是很牢固的。

当然了,也不排除有人窃取他人手机 SIM 卡的可能,如果成真的话还是很危险的,如果教育考试院未增加下载鉴权的功能,考生的志愿真的是有被修改的可能性的。

数据整理 & 小结

笔者使用工具将下载的 5000 余份准考证中的数据进行提取,并整理为表格,在墙内一社交媒体平台进行了发表。不出所料,官方可靠的消息 引起了部分轰动。大家主要震惊并疑惑于「为什么数据看起来是真的,实际也是真的」&「数据来源」两点。笔者担心对考试院权威性及对社会的不利影响,并未在平台公布考试院的接口鉴权漏洞。时至今日,仍然没有人知道那泄漏的五千份数据来源自考试院,如果换一个人,可能远不止五千份,甚至出一个第三方权威整理,全市高考报名名单也不是没有可能。

说起来,再过几天,第二次听口考试就要开始了,考试院这一次将如何构建准考证下载入口呢?让我们拭目以待。