关于
各位知道的,口语100是个上古时期的文物,而它有一个网页版,经过一番体验,可以知道网页版也是同样的难用。
那么,这样的网站,它安全吗?
不难发现,大部分接口都没有身份验证,所以我们可以仅用学生账号来干一些奇妙的事情。
获取答案
经验证,查询答案接口并不会验证当前账号是不是老师,所以任何人都可以获取听力答案。下面以“10月4日初三听力”为例进行演示。
发送请求
首先我们需要知道这场考试的定义 ID,即 examDefind.id(服务端自己少打了一个 e),可以通过地址直接查看,如 https://016.kouyu100.com/jshawgy/gotoExam.action?examDefind.id=3563,问号后就是,所以这场考试的定义 ID 为 3563。

然后,我们就可以向 findListenExamListChangeMock.action 接口发送请求,具体为:
GET /jshawgy/findListenExamListChangeMock.action?examId=3563&examType=2&isMockMode=1将其中的 examId 改为刚刚获取到的 3563 并发送。
然后就得到了一大坨 JSON,而我们只关心其中 examList 数组的第 0 项,找到里面的 id 并记下它的值 315899,这就是这场考试使用的试卷 ID。至于其他的,如 bookName 是试卷标题,我们用不到。
而有了试卷 ID,我们就可以获取试卷题目:
GET /jshawgy/getListenGroupsByExamId.action?examId=315899将 examId 改为 315899,响应中就包含了试卷的题目以及答案。为防止答案泄露,故在此不做演示。
分析返回结果
至于如何从响应里找到答案,下面简单演示一下。
响应剥掉外层后的结构:
{ "groupList": [ { "chooseList": [ { "title": "What time is it now?", "answers": "C", "score": 1, "item1": "...", "item2": "...", "item3": "...", "id": 1993221 } ] } ]}answers 和 title 属于同一个对象,title 为本题题目,answers 为答案。题目文本和标准答案被打包在同一个数组元素里一起发了下来。
限制?
考试开始前、进行中、结束后各发一次同样的请求,返回内容一致,answers 字段始终存在。
也就是说,理论上可以在开考前拿到整张卷子的答案。
可能的修复
只需要将评分放在服务端,提交答案后只返回对错即可,最起码也要在考试结束前拒绝发送答案,而我们的口语100显然一种都没做。
获取成绩
答案本身不该给,成绩这条路的性质不太一样:数据确实该给,但给错了人。
另一个参数
GET /jshawgy/findExamStudentScores.action?examScoreForm.examId=3562&examScoreForm.orgId=10613&examScoreForm.orgType=1orgId 这名字看着挺抽象,实际就是班级 ID,而且是连号的,10604 到 10622 正好是九年级的十九个班,换一个数字换一个班。
服务端拿到这个参数之后什么也没做,它不检查当前账号属不属于这个班,也不检查是不是老师,直接拿去数据库里查,然后原样返回。
如:
orgId=10604,拿到九年级1班,65条记录orgId=10613,拿到九年级10班,74条记录- 换成别的班级 ID,拿到对应班级的记录
所以,一个学生账号可以看到全校任何一个班的成绩。
返回了什么
每条记录有59个字段,挑几个:
| 字段 | 含义 | 示例 |
|---|---|---|
realname | 姓名 | 同学 A |
studentId | 账号 | 100001 |
stuScore | 得分 | 20 |
startTime | 开始作答 | 22:15:38 |
endTime | 交卷 | 22:37:39 |
班级 ID 连号,考试 ID 也连号,两层循环套起来,整所学校的成绩就到手了。
还有两个 Excel 接口
GET /jshawgy/findStudentScoreByOrg.action?examScoreForm.examId=3562&examScoreForm.orgId=10613&orderByType=1GET /jshawgy/findAllStudentScore.action?examScoreForm.examId=3562&orderByType=1第一个是班级成绩单,22 KB。第二个是全校成绩单,363 KB,一次请求就够了。
响应是二进制流,开头是 d0 cf 11 e0,OLE2 复合文档的魔数,也就是老式 .xls。文件元信息里甚至还留着生成工具的名字:Java Excel API v2.6.1。
比 JSON 接口还省事,不用分页不用拼接,下载下来用表格软件打开,全校数据就有了。
一个坑
这个接口对不存在的考试 ID 不返回空也不报错,而是回落到某个默认数据集,照样返回74条记录,只是 examName 和截止时间变成了 null。
不注意很容易把假数据当真。判断方法是看第一条记录的 examName 是不是 null。
根因
答案和成绩表面上是两类问题,往下看是同一个:服务端没有自己判断你是谁,而是听客户端复述。
三个地方犯的是同一个错。
跨站单点登录令牌是明文。叫 wwwToken,值是一段 base64:
eyJ1aWQiOiI1NDk3NTAiLCJkbSI6ImpzaGF3Z3kiLCJ1cmwiOiIwMTYua291eXUxMDAuY29tIn0=解码完是:
{"uid":"100000","dm":"example","url":"example.com"}只有用户 ID、域名、目标主机三个字段,没有签名,没有有效期,也没有防篡改手段。改掉 uid 重新编码,就是另一个人的身份。
媒体端身份是客户端拼的 JSON。上传视频时返回的地址里带了一串 base64 参数,解开是一段完整的身份描述,包含用户 ID、域名、姓名、学校、机构 ID。服务端直接把这些值写进了返回页面的 JavaScript 变量:
var userId = "100000";var userName = "示例用户";构造同样结构的 JSON,服务端就会认。
登录成功与否,客户端自己判断。密码错误返回 HTTP 200,账号不存在也返回 HTTP 200,只有正文里一个是 ERROR2 一个是 ERROR3。只看状态码的客户端会认为三次都登录成功了。
三件事的共同点是校验责任被推到了客户端。
其他
家长端子域。jiazhang 子域有个接口不带 Cookie 就能调,传用户 ID 返回真实姓名、班级和完整手机号。用户 ID 连号,等于一个全校通讯录查询器。
教师页面直达。普通学生账号访问 gotoUserManage、gotoChangeExam、gotoExamMonitor、gotoExamTeacherMark 全部返回 200,页面正常渲染。敏感的写操作接口另有一层校验,但可读内容已经足够多了。
失败响应没有区分度。“没登录”和“登录了但没权限”在响应上完全一样,都是 HTTP 200 加空响应体。探测时无法区分这两种情况,可以一直用匿名身份试所有接口。
最后
这个站点没有复杂的加密绕过,没有内存破坏,也不需要零日漏洞。
问题可以概括成:该在服务端做的判断被放到了客户端。
答案不该跟题目一起下发,这是数据问题。成绩该按班级过滤,这是权限问题。身份该由服务端签发并验证,这是信任问题。三个问题也是一个都没做对。
判断一段返回数据的代码有没有问题,只要问一句:这个请求凭什么能看到这些数据。如果答案是“因为它带了个 Cookie”或者“因为参数里写了这个 ID”,那就已经出问题了。
仅用于学习目的,禁止泄露任何答案及他人隐私数据,违者自行承担相应责任。
部分图片由 AI 生成。