流量分析
首先就是分析在答题过程中究竟有哪些流量,哪些流量是必须的,哪些流量是无关紧要的。首先使用了Wireshark,发现乱七八糟的包太多了,过滤协议玩得不溜,尝试使用Charles进行抓包。
Charles配置和使用
首先手机网络设置要添加代理:
其中服务器地址填charles所在设备的ip,端口填下面代理设置里的端口。
代理-代理设置:
为了抓https的流量,还需要ssl代理设置:
同时在目标设备上安装证书:
第一个是在本机安装,抓取本机流量,注意导入钥匙串后要打开证书简介设置信任:
第二个是在手机或者其他设备上通过浏览器下载安装,访问chls.pro/ssl 下载并安装证书。注意,在iOS10及更高版本中您必须进入"设置" > 通用 > 关于 > 证书信任设置并启用Charles证书。
这样手机上的http和https流量都可以被抓取了。
分析请求逻辑
首先,我使用账号密码登录软件,抓到了登录的请求和返回:
打开答题界面,开始答题,同时在Charles里也抓到了所有试题的文本:
因为一天只有一次答题机会,此时我已经把今天的“答”完了,所以试题没有截图,只有保存的文字版本:
{
"status": 20000,
"data": {
"examId": "******************",
"amId": "******************",
"ques": [{
"optionId": "714465123119681542",
"content": "灭火器种类的正确选择对于初起火灾的扑灭以及火灾的前期自救有着重要的意义,下列关于灭火器的选择,说法正确的是( )。",
"answerOption": "d09e1f14e9e53e16f6bc3c602127548a,b779cda5ffe64bf76042b024d0226d2a",
"correctAnswerOption": "",
"type": 2,
"analysis": "",
"options": [{
"answer": "清水灭火器适用于扑救可燃固体物质火灾",
"option": "A"
}, {
"answer": "轻金属如钾、钠等火灾不能用水流冲击灭火,只能用干粉灭火器或砂土掩埋",
"option": "B"
}, {
"answer": "二氧化碳灭火器适用于贵重设备火灾的扑灭",
"option": "C"
}, {
"answer": "泡沫灭火器可用于甲烷气体火灾的扑灭",
"option": "D"
}]
}, {
"optionId": "714465123195179018",
"content": "有机过氧化物、金属过氧化物着火时,可用水进行扑救。",
"answerOption": "ac4f382fe75f6a771fbba9674ac59385",
"correctAnswerOption": "",
"type": 0,
"analysis": "",
"options": [{
"answer": "对",
"option": "A"
}, {
"answer": "错",
"option": "B"
}]
},
...
...
发现要想获取题目,除了examType=1像是固定参数外,还需要activityId、memberId和amId。那就逆着一个个找吧。
发现了得到amId的请求:
想要得到amId,还需要activityId、memberId、deptId和deptEmpId。
之后又发现了得到deptId和deptEmpId的请求:
需要memberId和token,这两个字段登录的时候就可以得到。这样就剩activityId不知道是啥了,从名字上也不好判断,通过搜索activityId的值最终找到了相关请求:
怪不得找不到,原来换了变量名,我!@#$%^&...那么来捋一下流程:
userName+password ----> memberId+token ----> activityId+deptId+deptEmpId
activityId+deptId+deptEmpId+memberId ----> amId
activityId+memberId+amId ----> 试题
然后通过提交抓包可以知道,提交的data也就是在上面请求的试题data基础上,每道题添加一个questionAnswer字段:
data = '{
"examId": "******************",
"amId": "******************",
"ques": [{
"optionId": "714465123119681549",
"content": "依据《消防法》的规定,商场营业期间发生严重火灾时,下列灭火救援的做法,正确的是( )。",
"answerOption": "d1e3efd758a9b69b0d92aef6b1d70f4c,f9d7452981757e765ef3871a1f9db7b4,18675c5d659e26f47c4c7e5f99f8f7d4",
"correctAnswerOption": "",
"type": 2,
"analysis": "",
"options": [{
"answer": "商场员工立即组织、引导在场人员疏散撤离",
"option": "A"
}, {
"answer": "商场组织火灾现场扑救时,优先抢救贵重物品",
"option": "B"
}, {
"answer": "消防队接到报警后立即赶赴现场、救援遇险人员,实施扑救",
"option": "C"
}, {
"answer": "医疗单位及时赶赴现场,实施伤员救治",
"option": "D"
}],
"questionAnswer": "A"
}]
}'
搞清楚流程的逻辑后,问题来了:答案怎么搞?总不能去网上搜吧...
代码审计解密答案
注意到每个题目里都有个answerOption字段,里面的值都是md5加密的,数量1个到4个都有,猜想这就是答案。直接解密是乱码,估计是加盐了。于是第二天晚上我从头开始找加密函数在哪儿,最终定位到了关键代码:
setCorrectAnswer: function (t) {
"weChat" !== this.formPage && (clearInterval(this.timer), this.timer = null), this.showAnswerParsing = !0;
for (var e = this.chooseOption, s = this.questionList[this.current], i = s.options, n = [], r = s.answerOption, a = "encryptionhxakactivityqueskey" + s.type + s.optionId, o = 0; o < i.length; o++) {
var c = i[o].option;
if (2 === s.type) {
for (var l = r.split(","), u = 0; u < l.length; u++) if (_()(a + c) === l[u]) {
n.push(c);
break
}
} else if (_()(a + c) === r) {
n.push(c);
break
}
}
...
...
凭借我辣鸡的js水平,还是勉强看懂了逻辑。
首先,i是当前题目选项的list,里面包含了选项和选项内容,r是当前题目正确选项进行加密后的字符串相加并用逗号分隔,a是"encryptionhxakactivityqueskey"、该题类型(判断是0,单选是1,多选是2)、该题目的唯一id三个字符串相加,l是当前题目正确选项进行加密后的字符串的list,如果a和该题的某个选项字符串相加等于l里的某个值,则push这个选项,那么,就可以推出answerOption里每个md5的加密算法:
md5("encryptionhxakactivityqueskey"+该题类型+该题Id+选项)
之后验证了几道题,果然如上所述。至此,该答题平台的代码逻辑都搞清楚了,于是我用python写了个脚本,自动登录、进入答题、提交,直接返回满分,解放双手!
后记
能够搞定这个,全靠它没有啥安全机制,并且答案的验证全在前端的js代码里,可能开发者是想减轻服务器的负担吧。
评论