破解易班校本化签到

原起 —— 为什么对易班晚签下手

学校还在用易班进行晚点名,每天晚上9点都得在指定范围内签到
有时候一忙就给忘了绝对不是打游戏,忘签还要扣分,原信工系的我肯定要找一点解放双手的方法出来
先在全世界最大的同性交友网站上搜索,自动脚本结果全都是疫情期间上报健康情况的脚本,且都是好几年前根本没法使用。遂开始自行编写脚本之旅

过程 —— 分析如何实现目标

吾爱上混迹了好些年,自己也跟着破解使用了不少app,对app破解也有一定经验,以下是详细分析过程:

第一步 – 获取签到数据

获取签到秘钥

根据同性交友网站上的指点,易班使用RSA算法对用户密码进行加密,现有方法里RSA秘钥都已经失效,无法签到成功(反正我无法成功登录),那么第一步就应该从apk内获取到RSA秘钥,
这里我使用算法助手拦截应用内所有加密算法设置算法助手
通过搜索自己登陆的明文密码,看一下哪有调用

搜索明文密码

加密方式

查找秘钥

获取到秘钥

搜索结果里有2处调用,但RSA秘钥都是一样的,同时也获取到加密加密方式,使用小黄鸟抓包获取签到所需参数

利用Python模拟登陆:

# -*- coding: utf-8 -*-
import requests
from Crypto.Cipher import PKCS1_v1_5
from Crypto.PublicKey import RSA
import base64
def encryptPassword(pwd):
    # 密码加密
    PUBLIC_KEY = '''-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzq0rgsM++ZxLRGHpdfre
Hu6UXhdlUS5P2WOxRG14qU8/iWSb/CkOqgOl8AGcOhlthkvolCdpUvVcVsVUxBv0
YRN0Jb64zPrn5aLVwQT4RJn5tXvoqLdHIXis7pljXAMDPVZOVlWJkDMk8YU6HDaA
MqsD6l5p9lg2LMP4OhMgaPX+CkO370LB5vRjJTHp03n+IqfxXoC7DEd+kxRIEM2C
EDgUSYDJBDgwBvGALZmvB/a1b0im9t1P/EmnuE7uN9NRFoWyVpOiEwo/Ti7rmJGf
qNT3vvtfWo4nXsm1rYQXsPayoKDSRaba3gFY/1SYWLAuSO2q2da5ZCcsAk5RKy0V
c1hUg8n6y0YLAvuzoXY5VyNMXkhH5Zc5Kg64b5RxILeZpZG0MV7GFY3sw//k7SNg
darKT8A0Iv3l3lfguX3HNi6dkf97kS/EiA0tbkIB/JNjv13mq8HL7LijRt2hkKqP
PhQW88xC/exZilU5pAavoZOPuZIOTUHqtpRq4ZeKl+wDf+e5lPYFDpihWGjplGpa
4BOSmGeo/SyVFPji9QF4Pk0DRJF/NjwJoAC60xHAVt5Z4gQSOOOjNZDCswA0ry2L
e8m5cv5vPGY75uVrGqALQ6Xm961PPc5cJ1q7tmEZMj+z5HE7tgAdhiPI6acKgrAv
+1k4N0OVqKamMS+PVpD05hUCAwEAAQ==
-----END PUBLIC KEY-----'''
    cipher = PKCS1_v1_5.new(RSA.importKey(PUBLIC_KEY))
    cipher_text = base64.b64encode(cipher.encrypt(bytes(pwd, encoding="utf8")))
    return cipher_text.decode("utf-8")
def login():
    params = {
        "mobile": "",			 # 必要,登录手机号
        "password": encryptPassword(""), # 必要,RSA加密后的登录密码,如 "password": encryptPassword("123456"), 
        "ct": "2",			 # 必要,固定参数
        "identify": "0",		 # 必要,固定参数
        }
    HEADERS = {
        "AppVersion": "5.0.17"		 # 必要,由最新app版本得到
        }
    COOKIES = {}
    # 配置登录数据
    response = requests.post("https://m.yiban.cn/api/v4/passport/login", data=params, allow_redirects=False, cookies=COOKIES, headers=HEADERS).json()
    if response is not None and response["response"] == 100:
        access_token = response["data"]["access_token"]
        HEADERS["Authorization"] = "Bearer " + access_token
        COOKIES["loginToken"] = access_token
        print('用户信息:',response,"\n")
        print('loginToken:',access_token,"\n")
if __name__ == '__main__':
    login()
    
#输出######
用户信息: {
	'response': 100,
	'message': '请求成功',
	'is_mock': False,
	'data': {
		'user': {
			'sex': '1',
			'name': '***',
			'nick': '易班16043*****',
			'pic': {
				's': 'http://img02.fs.yiban.cn/4568****/avatar/user/68',
				'm': 'http://img02.fs.yiban.cn/4568****/avatar/user/88',
				'b': 'http://img02.fs.yiban.cn/4568****/avatar/user/160',
				'o': 'http://img02.fs.yiban.cn/4568****/avatar/user/'
			},
			'user_id': 4568****,
			'phone': '***********',
			'authority': '1',
			'isSchoolVerify': True,
			'school': {
				'isVerified': True,
				'schoolName': '*****学院',
				'schoolId': 310 ** ,
				'schoolOrgId': 2005 ** * ,
				'collegeName': '***系',
				'collegeId': ** ** * ,
				'className': '',
				'classId': 0,
				'joinSchoolYear': '2020',
				'type': 1
			}
		},
 		'access_token': 'cdd6d4432743414c9e1faf3e792*****',
		'urlBlackListLastUpdateTime': 0
	}
}
其中'access_token': 'cdd6d4432743414c9e1faf3e792*****' 是获取cookie的必须参数
写入COOKIE "loginToken" = “cdd6d4432743414c9e1faf3e792*****”

第二步 – 获取易班COOKIE

此步骤可直接参考GITHUB上的现成代码

def auth(self):
    COOKIES = {}
    CSRF = "00000000000000000000000000000000"
    HEADERS = {
        "Origin": "m.yiban.cn",
        "origin":"api.uyiban.com",
        "origin":"https://c.uyiban.com",
        "authority": "api.uyiban.com",
        "AppVersion": "5.0.17",
        "x-requested-with": "com.yiban.app",
        "user-agent":"Mozilla/5.0 (Linux; Android 12; XIAOMI Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/112.0.5615.48 Mobile Safari/537.36;webank/h5face;webank/1.0 yiban_android/5.0.17"
        }
    iapp = requests.get("http://f.yiban.cn/iapp/index?act=iapp7463", headers=HEADERS, allow_redirects=False, cookies=COOKIES) # 利用 loginToken 访问获取 verifyRequest跳转数据
    # 此处cookie带有 "loginToken" = “cdd6d4432743414c9e1faf3e792*****”
    act = iapp.headers["Location"] # 返回302跳转目标
    verifyRequest = re.findall(r"verify_request=(.*?)&", act)[0] # 正则取302跳转目标,得到 verify_request 数据
    json = requests.get("https://api.uyiban.com/base/c/auth/yiban?verifyRequest=" + verifyRequest + "&CSRF=" + CSRF, headers=HEADERS, allow_redirects=False, cookies={'csrf_token': CSRF})
    cookies = requests.utils.dict_from_cookiejar(json.cookies) # 获取cookie
    Attendancecookies = cookies # 签到cookies赋值self.Attendancecookies
    print('Location:',act,"\n")
    print('cookies:',Attendancecookies,"\n")
    
    ###打印数据:
    Location: https://c.uyiban.com/#/?verify_request=c9e814****53de92ccc', 'cpi': 'eyJD******%3D%3D', 'is_certified': '1'}    
    cookies: {'PHPSESSID': 'fa1a28bc38cc**c4b1e87a8ce51f****', 'cpi': 'eyJDaG**%3D%3D', 'is_certified': '1'}
    接下来签到仅需要 PHPSESSID

第三步 – 获取签到信息

易班app内校本化似乎使用iapp,本人利用算法助手 小黄鸟等均无法正常抓包。这个时候,我发现可以利用ADB连接手机,用chrome://inspect/#devices调试获取到接下来操作所需的信息:

校本化页面

签到页面

通过分析控制台可以得出2个非常重要的链接:

GET固定需要CSRF 可以是任意32位字符,但不能为空
提交上述此请求必须带有cookie(++只需要PHPSESSID++)及特定headers

signIn

请求数据:
https://api.uyiban.com/nightAttendance/student/index/signIn?CSRF=(32位字符)
返回数据:
{
  "code": 0,	# 返回0有校本化晚签任务 返回999则校本化未授权
  "msg": "",	# 如已签到则返回 "已签到"
  "data": {
    "State": 1,
    "Msg": "未达签到时段",
    "OutState": "on",
    "Remark": "因受不可抗因素影响导致定位有误,允许在范围之外签到,但需填写原因并上传照片,必须在本校范围,本人站在宿舍门口露出宿舍门牌号的位置进行拍照后上传",
    "FileUrl": "https://res.uyiban.cn/2533a357c60f2949fa60d896bb4264dd/public/ninghtattendance/202109/14068d3bc5aa49115f0dca01d279d**e.png",
    "Type": "campus",
    "Position": [
      {
        "Id": "695652bc99ffcf3c16cc8b40dc62****",
        "UniversityId": "2533a357c60f2949fa60d896bb42****",
        "Campus": "**校区",
        "BuildingId": "None",
        "Type": "campus",
        "Title": "**学院(**校区)",
        "Address": "**学院(**校区)",
        "LngLat": "118.254877,26.583077",	#学校经纬度
        "Range": 300,
        "MapType": 2,
        "Points": [
          "118.2522103916109,26.586591497111847",
          "118.25298823222522,26.586193330554728",
          "118.25805224284528,26.584600650478848",
          "118.25933970317243,26.583180651969975",
          "118.25771964892749,26.582077262591984",
          "118.25699008807544,26.580916293593525",
          "118.25672019079332,26.580653035039877",
          "118.25655758187179,26.580159498580716",
          "118.25550280317668,26.579863255747707",
          "118.25429446801547,26.58022066603373",
          "118.25376741394405,26.580678821211876",
          "118.2531652580202,26.580868319414655",
          "118.25260065302257,26.58111058881208",
          "118.25177319154147,26.58144400823378",
          "118.25107849940662,26.582067667855167",
          "118.25012363299732,26.582432267291093",
          "118.24921168193225,26.58308950281557",
          "118.24800737008457,26.58372394739835",
          "118.24755407676105,26.584914870742125",
          "118.2478518019617,26.586220914427095",
          "118.24896491870288,26.586836152984155",
          "118.25036503180866,26.587512551792482",
          "118.2522103916109,26.586591497111847"
        ],	# 签到范围
        "AddressName": "自定义多边形",
        "CreateTime": 1596529630
      }
    ],
    "Range": {
      "StartTime": 1683810000,	# 签到开始时间戳
      "EndTime": 1683815400,	# 签到结束时间戳
      "SignDay": 1,
      "RelatType": 1,
      "RelatTimeType": 1
    },
    "IsNeedPhoto": 2,
    "AttachmentFileName": ""
  }
}

signPosition

请求数据:
https://api.uyiban.com/nightAttendance/student/index/signPosition?CSRF=(32位字符)

POST:
{
  "Code": "",
  "PhoneModel": "",
  "SignInfo": '{
    "Reason": "",
    "AttachmentFileName": "",
    "LngLat": "经度,纬度",	#  如 "LngLat": "118.254877,26.583077"
    "Address": "**地址"# 如 "Address": "*省*市*区*镇*学院(*校区)"
  }',
  "OutState": "1"
}
返回数据:

签到成功:

{'code': 0, 'msg': '', 'data': True}

签到失败:

{'code': 500, 'msg': '非法签到', 'data': None}

圆满 —— 目标实现代码

# -*- coding: utf-8 -*-
"""
@Time : 2023/4/14 11:40
@Auth : apecode.
@File : yiban.py
@Blog : https://wcyuns.cn
"""
import json
import re
import sys
import os
import time
import urllib
from urllib import parse
import random
import requests
import numpy
import jsonpath
import base64
import msvcrt
try:
    from Crypto.Cipher import PKCS1_v1_5
    from Crypto.PublicKey import RSA
except ModuleNotFoundError:
    print("缺少pycryptodome依赖!程序将尝试安装依赖!")
    os.system("pip3 install pycryptodome -i https://pypi.tuna.tsinghua.edu.cn/simple")
    os.execl(sys.executable, 'python3', __file__, *sys.argv)

def encryptPassword(pwd): # 登录密码加密
    # 密码加密
    PUBLIC_KEY = '''-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzq0rgsM++ZxLRGHpdfre
Hu6UXhdlUS5P2WOxRG14qU8/iWSb/CkOqgOl8AGcOhlthkvolCdpUvVcVsVUxBv0
YRN0Jb64zPrn5aLVwQT4RJn5tXvoqLdHIXis7pljXAMDPVZOVlWJkDMk8YU6HDaA
MqsD6l5p9lg2LMP4OhMgaPX+CkO370LB5vRjJTHp03n+IqfxXoC7DEd+kxRIEM2C
EDgUSYDJBDgwBvGALZmvB/a1b0im9t1P/EmnuE7uN9NRFoWyVpOiEwo/Ti7rmJGf
qNT3vvtfWo4nXsm1rYQXsPayoKDSRaba3gFY/1SYWLAuSO2q2da5ZCcsAk5RKy0V
c1hUg8n6y0YLAvuzoXY5VyNMXkhH5Zc5Kg64b5RxILeZpZG0MV7GFY3sw//k7SNg
darKT8A0Iv3l3lfguX3HNi6dkf97kS/EiA0tbkIB/JNjv13mq8HL7LijRt2hkKqP
PhQW88xC/exZilU5pAavoZOPuZIOTUHqtpRq4ZeKl+wDf+e5lPYFDpihWGjplGpa
4BOSmGeo/SyVFPji9QF4Pk0DRJF/NjwJoAC60xHAVt5Z4gQSOOOjNZDCswA0ry2L
e8m5cv5vPGY75uVrGqALQ6Xm961PPc5cJ1q7tmEZMj+z5HE7tgAdhiPI6acKgrAv
+1k4N0OVqKamMS+PVpD05hUCAwEAAQ==
-----END PUBLIC KEY-----'''
    cipher = PKCS1_v1_5.new(RSA.importKey(PUBLIC_KEY))
    cipher_text = base64.b64encode(cipher.encrypt(bytes(pwd, encoding="utf8")))
    return cipher_text.decode("utf-8")
####################################################################################################################################################################################################
####################################################################################################################################################################################################

class yiban:
    COOKIES = {}
    def __init__(self, mobile, password): # 全局变量
        self.mobile = mobile # 登录手机号
        self.password = password # 登录密码
        self.session = requests.session()
        self.HEADERS = {
            "Origin": "m.yiban.cn",
            "origin":"api.uyiban.com",
            "origin":"https://c.uyiban.com",
            "authority": "api.uyiban.com",
            "AppVersion": "5.0.17",
            "x-requested-with": "com.yiban.app",
            "user-agent":"Mozilla/5.0 (Linux; Android 12; Redmi K30 Pro Build/SKQ1.211006.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/112.0.5615.48 Mobile Safari/537.36;webank/h5face;webank/1.0 yiban_android/5.0.17"
            }
        self.Attendancecookies = {}
        self.CSRF = "00000000000000000000000000000000"


    def login(self): # 登录
        params = {
            "mobile": self.mobile, #登录手机号
            "password": encryptPassword(self.password), #RSA加密后的登录密码
            "ct": "2", #固定参数
            "identify": "0", #固定参数
        }
        # 配置登录数据
        response = requests.post("https://m.yiban.cn/api/v4/passport/login", data=params, allow_redirects=False, cookies=self.COOKIES, headers=self.HEADERS).json()
        if response is not None and response["response"] == 100:
            self.access_token = response["data"]["access_token"]
            self.HEADERS["Authorization"] = "Bearer " + self.access_token
            # 增加cookie
            self.COOKIES["loginToken"] = self.access_token
            print('用户信息:',response,"\n")
            print('loginToken:',self.access_token,"\n")
            print('用户姓名:',response["data"]["user"]["name"],"\n手机号码:",response["data"]["user"]["phone"])
            return 1
        else:
            print(response) 
            return self.mobile
        #返回https://f.yiban.cn/iapp/index?act=iapp7463所需的Cookie“loginToken=ce196e5fb2900bc35b44e1f1b4ed****”


        #通过获取的loginToken访问iapp后台得到值verifyRequest
        #利用verifyRequest访问api获取签到用的cookie
        #返回cookie内容范例
        #{'PHPSESSID': '1aef34b976315dca8400711255f1a9af', 'cpi': 'eyJDaGFubmVsIjoieWliYW......TliNWViZTUxYWMifQ%3D%3D', 'is_certified': '1'}
        #PHPSESSID为关键值,有这一个参数就可以访问签到等相关内容
        #cpi 使用base64编码 有个人相关信息
    def auth(self): # 获取签到cookie
        iapp = requests.get("http://f.yiban.cn/iapp/index?act=iapp7463", headers=self.HEADERS, allow_redirects=False, cookies=self.COOKIES) # 利用 loginToken 访问获取 verifyRequest
        act = iapp.headers["Location"] # 返回302跳转目标
        verifyRequest = re.findall(r"verify_request=(.*?)&", act)[0] # 正则取302跳转目标,得到 verify_request 数据
        json = requests.get("https://api.uyiban.com/base/c/auth/yiban?verifyRequest=" + verifyRequest + "&CSRF=" + self.CSRF, headers=self.HEADERS, allow_redirects=False, cookies={'csrf_token': self.CSRF})
        #访问api获取cookie CSRF值按此可用,只验证位数
        cookies = requests.utils.dict_from_cookiejar(json.cookies) # 获取cookie
        self.Attendancecookies = cookies # 签到cookies赋值self.Attendancecookies
        print('Location:',act,"\n")
        print('cookies:',self.Attendancecookies,"\n")
        return

    def authorization(self, testauth): # 授权校本化
        self.testauth = testauth
        authhtml = requests.get("https://oauth.yiban.cn/code/html?client_id=95626fa3080300ea&redirect_uri=https://f.yiban.cn/iapp7463") # 利用 loginToken 访问获取 verifyRequest
        HTML_PUBLIC_KEY = re.search('<input type="test" id="key" value="((?:.|\n)+)?" style="display:none">',authhtml.text)[1] # 获取网页签到加密 password 证书
        webcookies = requests.utils.dict_from_cookiejar(authhtml.cookies) # 获取校本化授权 cookie    登录需要
        print()
        print("获取的网页cookie:",webcookies)
        print("获取的网页加密证书:",HTML_PUBLIC_KEY,"\n")
        HTML_cipher = PKCS1_v1_5.new(RSA.importKey(HTML_PUBLIC_KEY))
        HTML_cipher_text = base64.b64encode(HTML_cipher.encrypt(bytes(self.password, encoding="utf8")))
        HTML_cipher_decode = HTML_cipher_text.decode("utf-8")
        print("密码网页加密结果:",HTML_cipher_decode)
        webparams = {
            "oauth_uname": self.mobile , # 账号
            "oauth_upwd": HTML_cipher_decode , # 加密后密码
            "client_id": "95626fa3080300ea", # 应用端编号
            "redirect_uri": "http://f.yiban.cn/iapp7463", # 应用端回调地址
            }
        webauth = requests.post("https://oauth.yiban.cn/code/usersure", data=webparams, cookies=webcookies, headers=self.HEADERS).json()
        print("授权结果",webauth,"\n")
        print("授权成功,返回获取签到信息中...")
        return

    def signPosition(self): 
        Position = requests.get("https://api.uyiban.com/nightAttendance/student/index/signPosition?CSRF=" + self.CSRF, allow_redirects=False, cookies={'PHPSESSID': self.Attendancecookies['PHPSESSID'], 'csrf_token': self.CSRF}, headers=self.HEADERS).json()
        if Position["code"] == 0: # 判断是否登录成功
            print("校本化已授权,有晚签任务\n")
            print("可签到数据:",Position,"\n")
            State=Position["data"]["State"] # 获取状态码
            Msg=Position["data"]["Msg"] # 获取返回信息
            StartTime=Position["data"]["Range"]["StartTime"] # 获取签到开始时间
            self.StartTime =  StartTime
            EndTime=Position["data"]["Range"]["EndTime"] # 获取签到结束时间
            pause = StartTime - int(time.time()) # 获取系统现在时间
            if State == 2: # 没有签到任务 如学校没有晚签
                print("您",Position["data"]["Msg"])
                return
            else:
                if State == 3: # 已签到
                    print("[*] 签到接口返回数据:",Position["data"]["Msg"])
                    return
                else:
                    self.Address=jsonpath.jsonpath(Position,'$...Address')[0] # 获取可签到位置 地址
                    self.LngLat=jsonpath.jsonpath(Position,'$...LngLat')[0] # 获取可签到位置 经纬度
                    self.lonss = '%.6f' %float(round(float(self.LngLat.split(",")[0]),3) + float(random.uniform(0.000100,0.000999))) # 根据地址随机修改经度后3位,达到每次定位位置不一样
                    self.latss = '%.6f' %float(round(float(self.LngLat.split(",")[1]),3) + float(random.uniform(0.000100,0.000999))) # 根据地址随机修改纬度后3位,达到每次定位位置不一样
                    if State == 0: # 已经到达签到时间
                        print("可以签到,立即执行!\n")
                        self.nightAttendance() # 签到跳转
                    else:
                        if State == 1: # 还未开始签到
                            pause = StartTime - int(time.time()) # 获取剩余时间
                            while pause >= 60 :
                                list = ["\\", "|", "/", "—"]
                                index = pause % 4
                                print("[*] 距签到时间还有 {} 秒 {}".format(pause,list[index]), end="\r", flush=True)
                                pause = StartTime - int(time.time())
                                time.sleep(1)
                            else:
                                while pause <= 60 and pause >=10 :
                                    print("[*] 距签到时间还有 {} 秒 {}".format(pause,list[index]), end="\r", flush=True)
                                    pause = StartTime - int(time.time())
                                    time.sleep(0.5)
                                else:
                                    while pause <= 10 :
                                        print("[*] 距签到时间还有 {} 秒 {}").format(pause,list[index], end="\r", flush=True)
                                        pause = StartTime - int(time.time())
                                        while pause <= 0.25 :
                                            print("到达签到时间!")
                                            self.nightAttendance() # 签到跳转
                                            return
                                        time.sleep(0.25)
            if Position["code"] == 999:
                print("[*] 晚签信息获取失败\n[*] 有可能是校本化未授权或您的签到页面非本脚本所适配")
                self.authorization()
        return 

    def nightAttendance(self): # 执行签到
        paramss = {
            "Code": "",
            "PhoneModel": "",
            "SignInfo": '{"Reason":"","AttachmentFileName":"","LngLat":"' + self.lonss + ',' + self.latss + '","Address":"' + self.Address + '"}',
            "OutState": "1"
            }
        nightsign = requests.post("https://api.uyiban.com/nightAttendance/student/index/signIn?CSRF=" + self.Attendancecookies['PHPSESSID'], data=paramss, cookies={'csrf_token': self.Attendancecookies['PHPSESSID'], 'PHPSESSID': self.Attendancecookies['PHPSESSID']}, headers=self.HEADERS).json()
        print("签到返回数据:",nightsign)
        #print(nightsign)
        return self.signPosition() # 跳转回signPosition验证是否签到成功

    def loginin(self): # 登陆校验
        # 应当先判断列表账号是否都能正常登陆
        loginin = self.login() # 登录
        if loginin != 1:
            print("用户",loginin,"登陆失败,请处理")
            print("按任意键退出脚本")
            ord(msvcrt.getch())
            sys.exit()
        self.authorization(0) # 校本化授权先执行
        return

    def setall(self): # 多人签到
        loginin = self.login() # 登录
        self.auth() # 授权
        self.signPosition() # 判断能否签到&自动跳转签到
        #self.authorization(0) # 校本化授权 已写入
        return

def main(): # 主函数,填写账号密码
    yiban("","").loginin() # 如 yiban("18655558888","123456").loginin() 
    time.sleep(0.5)
    print("#签到检查通过#################\n")
    yiban("","").setall() # 如 yiban("18655558888","123456").setall()
    time.sleep(0.5)
    print("##############################")
    print("签到任务已执行完成!")
    sys.exit()
if __name__ == '__main__':
    main()