学习某学院打卡请求流程

125 阅读7分钟
  • 主要目的:学习且了解网站加密方式

  • 次要目的:怎么通过技术手段,自动处理人情世故(DA KA)

  • 网站地址(Base64加密):aHR0cHM6Ly90Yi5nZGVpLmVkdS5jbi9sb2dpbg==

    • 参考网站:https://base64.us/

分析

登录成功后,进入打卡界面后,通过开发者工具,可以看到当点击每日健康填报中的点击一键打卡时,主要是发送一个Ajax请求 image-20220913111534037.png 点击数据包,观察请求地址和响应

  • 请求地址:https://xxxx/system/yqdc/yjtb? + _=xxx

    • 明显看出请求携带的查询参数是一个时间戳
  • 请求方式:GET

  • 返回响应:打卡成功-1 OR 打卡失败-3

image-20220913112224330.png

image-20220913112308149.png

image-20220913112417952.png 那么,明白了打卡原理后,首先想到的就是可以通过模拟浏览器发送请求,以此来达到人手动打卡的目的。

  • 模拟浏览器发送请求:requests模块

  • 那么模拟请求肯定不会那么简单,首先明确知道的是打卡请求携带的查询参数是时间戳

    • 时间戳:可通过time模块自动生成

      • str(int(time.time() * 1000))
        
  • 其次,请求时带上的请求头中一般除UA外,还要分析需不需要其他

  • 通过多次请求(退出登录后,重新登录打卡),查看数据包,观察得出请求头中的cookie是会发生改变的,也就是cookie是有时效期

    image-20220913122335385.png

    此时能想到的处理方法有两种:

    1. 查看js代码,了解cookie生成的本质(破解cookie生成)

    2. 基于Session对象实现自动处理

      • 那么使用session,就意味着session对象至少要使用两次,第一次使用session发送请求的过程中,如果产生了cookie,则cookie会被自动存储到该session对象中,而到了下次再次使用session对象发送请求时,该请求就会携带之前存储的cookie进行请求发送。

    那么第一次使用session发送什么请求才能拿到cookie呢?

    • 要知道,想打卡成功,前提是登录

    所以首先就要用session对象对登录发送请求

    在登录界面,随意输入账号和密码,点击登录后,在开发者工具中对登录数据包进行分析:

    • 这里输入的账号是(zhanghao),密码(123456)

    image-20220913202100356.png

    此时返回的数据包中,肯定是账号密码错误

    image-20220913203717399.png

    但没关系,此时主要想知道登录的时候,它对账号名和密码做了什么处理

    image-20220913203830649.png

    那么从数据包中,可以得知,登录的时候是发送了一个POST请求,并携带上的请求参数中,有之前输入的账号和密码,并且密码是通过加密的。

登录密码加密

所以主要来学习一下该网站是通过什么方式来加密密码的

  • 在开发者工具中,按Ctrl+Shift+F,调出搜索框,输入password进行全局搜索,可以看到许多js文件中都有password关键字,但其中有一个叫login.js的文件,就非常值得注意。因为此时我们就是在了解其登录时怎么加密的,所以加密的关键代码最有可能在这个login.js文件中。

    image-20220913211948175.png

  • 点进去,按Ctrl+F,输入password搜索,看到第一处中刚好有一个关键字眼encrypt,也就是加密的意思,所以有理由怀疑这里就是加密关键代码,于是在该行代码以及周围都打上断点。

    image-20220913212508568.png

  • 重新登录,观察断点位置,运行到最后一个断点处,就观察到password已经加密完,变成我们想要的形状。

    image-20220913212730333.png

  • 断点运行完,与数据包中的一对比,发现断点处的password就是登录最终加密的password

    image-20220913212853496.png

  • 所以在js代码中的login函数上面的,encrypt就是加密的方法。

改写逆向js代码

  • 那么既然已经了解encrypt就是加密的方法,接着就可以把encrypt所在的function函数里的代码都先粘贴进一个新建的js文件中,并进行略微改写。

     var password = $.common.trim($("input[name='password']").val())
     该代码里的$("input[name='password']",就相当于最开始输入的密码,并赋给一个password的变量
     ```
    
    $("input[name='password']").val(encrypt.encrypt(password));
    该代码就把输入的密码,加密后重新赋值回原来存代码的input标签中
    ```
    

    略微改写:

    function getPwd(password) {
    var kk = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCaGxxomLHQmUzALgHx3tV0L/zY0fabQJVPWJeW0O+KBsQdeP/HC015zV6C4ExiBxHkA8RtnSfMkKKBH3/6dyatFCrCxIw5KgtNqmC9dtsdUmDJq8IDW7hFkODAyzDAvuovvGn+wQvRl97iEqUVstnLi7SD9zCuM39AUM4ugtqsBwIDAQAB";
    var encrypt = new JSEncrypt();
    encrypt.setPublicKey(kk);
    var password = $.common.trim(password);
    var pwd = encrypt.encrypt(password);
    return pwd
    }
    

    其中$.common.trimnew JSEncrypt()都是未知的。

  • 碰到不知道的东西,都可以在开发者工具的Console中,输入查看,那么很明显

    • $.common.trim就是下图的函数

    image-20220915230001562.png

  • 改写为一个函数:

    function trim(value) {
    if (value == null) {
        return "";
    }
    return value.toString().replace(/(^\s*)|(\s*$)|\r|\n/g, "");
    };
    
  • 再把之前的修改一下

    var password = $.common.trim(password);
    就可以变为
    var password = trim(password);
    
  • 然后encrypt.encrypt(password)中,encrypt方法具体干了什么,就可以把鼠标放上去悬停,然后点击进入encrypt这个方法里,查看

    image-20220913213143343.png

  • 可以看到encrypt也是一个函数方法

    image-20220917225431411.png

  • 但它是被一个叫rt的函数给包裹住了

    image-20220917225531198.png

  • rt这个函数外面也有一层大函数包裹,所以要想正常,简单的使用,只需要把encrypt关键字所在文件的全部代码复制粘贴,然后根据运行报错,修改即可。

  • 在改写的js代码最后,调用创建的getpwd函数,根据运行报错调试。

    • 调用代码console.log(getPwd('123456'))
    • 运行时可能报错:
    1.
    "Microsoft Internet Explorer" == navigator.appName ? (O.prototype.am = function(t, e, i, r, n, s) {
                                         ^
    ReferenceError: navigator is not defined
    `'navigator' 未定义`
    解决方法:
    定义它:navigator     javascript内置对象
    在最开头写navigator = this;
            
    2.
            if (window.crypto && window.crypto.getRandomValues) {
            ^
    ReferenceError: window is not defined
    `'window' 未定义`
    window   javascript内置对象
    解决方法:
    在最开头写window = this;
                
    3.
    var encrypt = new JSEncrypt();
                  ^
    ​
    ReferenceError: JSEncrypt is not defined
    在最开头写JSEncrypt = this;
                
    在这时候,改写就已经成功,可以使用execjs来调用该js代码,对密码进行加密
    但是该代码可能在node.js中运行不成功
    依然报错:
    var encrypt = new JSEncrypt();
                  ^
    TypeError: JSEncrypt is not a constructor
    这时它找到了JSEncrypt,但说他不是一个构造函数。
    解决方法:
        window.JSEncrypt = rt,
        t.JSEncrypt = rt,
            
            window.或t.随便删一个
    这样在node.js中就能成功运行
    

    image-20220918085233725.png

运行成功:

image-20220918085607776.png

实现人情世故

  1. 创建seesion对象,携带上请求头headers,请求参数data,对登录地址,发起post请求。

    • verify=False 移除SSL认证
      

    image-20220918091802130.png

  1. 登录请求成功后,就对打卡地址进行get请求,携带上查询参数时间戳

    image-20220918092002852.png

    打卡有两种返回结果,成功和不成功,所以可加上异常捕获

    不成功就是3,就可以发送邮件信息提醒(也可以短信啥的),手动打卡

    发送邮件失败,就写入日志,等别人提醒你打卡,哈哈。

    不过因为这个是个人版,一天只打卡一次吧,只要程序逻辑正确,一般没啥意外

    image-20220918092307917.png

  1. 有始有终,登录过了,就退出登录

    image-20220918092933571.png

    allow_redirects=False 禁止302跳转
    
  1. 最后,如果想每天都自动打卡,就把程序部署上服务器。

    定时代码:

    daka(password.login_username, password.login_password)对打卡过程封装

    while True:
        now_hour = time.strftime("%H", time.localtime())
        now_min = time.strftime("%M", time.localtime())
        if now_hour < "08":
            rest = 8 - int(now_hour)
            sleeptime = (rest - 1) * 3600 + (60 - int(now_min)) * 60
            print("启动时北京时间为:" + time.strftime("%H:%M", time.localtime()), "\t 打卡程序将在", rest - 1, "小时",
                  int((sleeptime - (rest - 1) * 3600) / 60), "分钟后运行")
            time.sleep(sleeptime)
            daka(password.login_username, password.login_password)
        elif now_hour > "08":
            rest = 8 - int(now_hour) + 24
            sleeptime = (rest - 1) * 3600 + (60 - int(now_min)) * 60
            print("启动时北京时间为:" + time.strftime("%H:%M", time.localtime()), "\t 打卡程序将在", rest - 1, "小时",
                  int((sleeptime - (rest - 1) * 3600) / 60), "分钟后运行")
            time.sleep(sleeptime)
            daka(password.login_username, password.login_password)
    ​
        elif now_hour == "08":
            print("启动时北京时间为:" + time.strftime("%H:%M", time.localtime()), "\t 打卡程序将在每天 8 点运行!")
            # 以下为定时任务
            daka(password.login_username, password.login_password)
            time.sleep(86400 - int(now_min) * 60)
    

    服务器是Ubuntu的话,持续运行python程序方法:

    1. 后台启动python程序

      nohup python -u main.py > main.out 2>&1 &
      
    2. 打印输出

      tail -f main.out
      

      退出Ctrl+C

    3. 停止python程序

      • 查看pid

        ps -ef | grep python
        
      • 杀死pid

        kill -9 xxx
        

以上只是提供一个思路,并没有其他想法。