MENU

掌上长理晚寝签到 PHP 模拟签到源码

2018 年 11 月 13 日 • 技术流

今天收到通知,“掌上长理” APP 的晚寝签到功能暂停使用了。我之前说过所谓的晚寝签到很好破解。尽管其意在使用蓝牙 iBeacon 作为验证,但实际上只需简单构造 HTTP 请求即可模拟签到。如果要说有什么难的,那就是关于 HTTPS 的抓包了,这部分不是本文要讨论的。总之,这个功能做得不用心,最初甚至没有检验时间,学生能提前签到(beyondclock)。最大的缺点则是三天两头就崩溃,不具备现实可用性。

有学长已经把他的源码公开了(文末),于是我也决定公开我的代码,来加速这种不人性制度的灭亡(误)。进行脱敏处理后,我只放出 PHP 部分,而 HTML 表单、前端 Cookie 处理等请自行构造。脱敏修改后的代码未经过测试。

特性

  • 依赖于 php-curl 组件;未严格遵循设计模式(原本是自用的)
  • 过滤了万一出现的官方 XSS 注入代码
  • 全部模拟参数可自定义,也提供蓝牙标识自动选择模式
  • 支持慢速模式(slowdown),在时延和行为上与人类更加接近
  • 为命令行 cURL 使用而优化,增加可扩展性(如配合实现定时签到)

源码

<?php

    $warning = '';
    $isCurl = isset($_SERVER['HTTP_USER_AGENT']) && strpos(strtolower($_SERVER['HTTP_USER_AGENT']), 'curl') === 0;

    function myFunc(){
        if(empty($_POST['stuid']) || empty($_POST['password']) || empty($_POST['bluetooth']) || empty($_POST['device']) || empty($_POST['os']))
            return '存在空项,请填写完整后再试。';
        if(strlen($_POST['stuid']) != 12 && strlen($_POST['stuid']) != 18 && strlen($_POST['stuid']) != 11)
            return '“学号”长度有误,须为12位,或者为身份证号或手机号。';
        if(strlen($_POST['bluetooth']) != 20 && $_POST['bluetooth'] != 'AUTO')
            return '“捕获蓝牙”标识长度有误!';
        if(strlen($_POST['device']) != 14)
            return '“手机标识”长度有误!';
        
        $stuid = $_POST['stuid'];
        $password = $_POST['password'];
        $bluetooth = strtoupper($_POST['bluetooth']);
        $device = $_POST['device'];
        $os = $_POST['os'];
        $slowdown = isset($_POST['extra']) && is_array($_POST['extra']) && in_array('slowdown',$_POST['extra']);
        $beyondclock = isset($_POST['extra']) && is_array($_POST['extra']) && in_array('beyondclock',$_POST['extra']);

        $ch = curl_init();
        $host = 'https://csust.edu.chsh8j.com'; //旧:8088
        
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE); 
        curl_setopt($ch, CURLOPT_HEADER, FALSE); 
        curl_setopt($ch, CURLOPT_NOBODY, FALSE);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1); //重要
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, FALSE);
        curl_setopt($ch, CURLOPT_COOKIESESSION, FALSE); //TRUE
        curl_setopt($ch, CURLOPT_FORBID_REUSE, FALSE); //TRUE
        curl_setopt($ch, CURLOPT_FRESH_CONNECT, FALSE); //TRUE
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        curl_setopt($ch, CURLOPT_TIMEOUT, 12);
        //$httpCode = curl_getinfo($ch,CURLINFO_HTTP_CODE);

        $header_data = array(
            'Host: csust.edu.chsh8j.com',
            'Accept-Encoding: gzip',
            'User-Agent: okhttp/3.4.1',
            'Content-Type: application/x-www-form-urlencoded',
            'Connection: keep-alive'
        );
        $post_data = array(
            'params' => json_encode(array('password'=>$password,'userName'=>$stuid))
        );
        curl_setopt($ch, CURLOPT_URL, $host.'/magus/appuserloginapi/userlogin');
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header_data);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
        $output = curl_exec($ch);
        $json = json_decode($output, true);
        if(!is_array($json) || empty($json['result']['message'])) return '登录时发生错误,可能是官方服务器崩了。';
        if(empty($json['result']['token'])) return htmlspecialchars($json['result']['message']);
        $token = $json['result']['token'];

        if($slowdown){
            $post_data = array(
                'params' => json_encode(array('appversion'=>'','token'=>$token))
            );
            curl_setopt($ch, CURLOPT_URL, $host.'/magus/commonalityapi/queryAppHomeData');
            curl_setopt($ch, CURLOPT_HTTPHEADER, $header_data);
            curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 2);
            curl_setopt($ch, CURLOPT_TIMEOUT, 3);
            curl_exec($ch); //无需保存

            usleep(rand(500000,2000000));
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
            curl_setopt($ch, CURLOPT_TIMEOUT, 12);
        }

        $header_data[] = 'token: '.$token;
        $post_data = array(
            'token' => $token //这个好像不重要
        );
        curl_setopt($ch, CURLOPT_URL, $host.'/dorm/app/dormsign/sign/student/detail');
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header_data);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
        $output = curl_exec($ch);
        $json = json_decode($output, true);
        if(!is_array($json) || empty($json['data'])){
            if(strpos($output, '登陆已经失效') !== false) return '已经登录,但官方随后拒绝相认,请重试。'; //有时容易出现登录失效,故在此判断 {"data":null,"message":"登陆已经失效","status":11051}
            else return '获取签到信息时发生错误,可能是官方服务器崩了。';
        }
        $data = $json['data'];
        if(!isset($data['sponsorStatus'])) return '已获取签到信息,但没有待完成的签到。';
        if(!$beyondclock && $data['sponsorStatus'] != '1') return '有待完成的签到,但还未开始('.( !empty($data['startTime'])?'宣称开始于 '.htmlspecialchars($data['startTime']):'未宣称开始时间' ).')。';
        if($bluetooth == 'AUTO'){
            if(empty($data['bleinfoList'])) return '自动获取蓝牙标识列表失败!';
            $temp = $data['bleinfoList'];
            shuffle($temp);
            if(empty($temp[0]['bleId'])) return '官方好像提供有蓝牙标识,但获取失败!';
            $bluetooth = $temp[0]['bleId'];
        }else{
            if(strpos($output, '"'.$bluetooth.'"') === false) return '“捕获蓝牙”标识不在官方列表中!';
        }
        if(empty($data['signId'])) return '处理签到信息时发生未预期的错误!';
        $signId = $data['signId'];

        if($slowdown) usleep(rand(500000,5000000));

        $post_data = array(
            'signId' => $signId,
            'bleId' => $bluetooth,
            'devUuid' => $device,
            'token' => $token,
            'osName' => $os
        );
        curl_setopt($ch, CURLOPT_URL, $host.'/dorm/app/dormsign/sign/student/edit');
        curl_setopt($ch, CURLOPT_HTTPHEADER, $header_data);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
        $output = curl_exec($ch);
        $json = json_decode($output, true);
        if(!is_array($json) || empty($json['data'])){
            if(strpos($output, '登陆已经失效') !== false) return '已经获取签到信息,但官方随后拒绝相认,请重试。'; 
            else return '尝试签到时发生异常!';
        }
        $data = $json['data'];
        if(empty($data['message'])) return '尝试签到时可能发生异常!';
        $data = $data['message'];

        if($data == '签到成功')
            $data = '<span class="green">恭喜,签到成功!'.( $_POST['bluetooth']=='AUTO'? '使用的“捕获蓝牙”标识是: '.$bluetooth :'' ).'</span>'; //排版原因不用全角冒号
        else
            $data = htmlspecialchars($data);
        curl_close($ch);

        return $data;
    }

    /* TEST
    if($isCurl){
        print_r($_POST);
        exit;
    }
    */

    if($_SERVER['REQUEST_METHOD'] == 'POST'){
        $warning = myFunc();
        if($isCurl){
            $replace = array(
                '<span class="green">' => '',
                '</span>' => ''
            ); 
            echo htmlspecialchars_decode(str_replace(array_keys($replace), $replace, $warning));
            exit;
        }else{
            setcookie('warning', $warning, 0); //当前目录有效,中文无需编码
            header('Location: https://'.$_SERVER['HTTP_HOST'].$_SERVER["REQUEST_URI"]);
            exit;
        }
    }else{
        if($isCurl){
            echo '本工具支持 cURL,可以 POST 提交使用。';
            exit;
        }else{
            if(!empty($_COOKIE['warning'])){
                $warning = $_COOKIE['warning'];
                setcookie('warning', '', 1);
            }
        }
    }

?>

源码可以自由使用,只要不太过分。

其他版本

有学长和学弟写了其他语言的版本,在此我加上链接——

文章发布之后增加了 Python 版本链接。

添加新评论

已有 8 条评论
  1. 徐大佬NB!

  2. Lindsay Lindsay

    em...报告一下网站bug(算是吗?)在Tim的内置浏览器中发布评论不会显示待审核的评论,就酱紫。

    1. @Lindsay手机 QQ 的内置行为比较怪,这个主题也不是我写的,我懒得只会跟进官方改进。感谢报告啦,但暂时不会修复咯。

    2. Lindsay Lindsay

      @Bro.Xu可能是因为之前挂着代理在访问的原因,评论的时间也错乱了哈

  3. Lindsay Lindsay

    貌似就是使用 web api 的那个原理?登陆一下记录各种 cookies,然后用来签到?我看懂的就这些

    1. @Lindsay就是构造相应的 HTTP 请求(主要是 POST 请求)提交。token 什么的算是常规操作,只是这里它不用 Cookie,而直接写在了 header。

  4. 兹磁呀,必须兹磁呀(

    1. @hugefiver捕捉大佬一枚🐸