CUMT教务系统模拟登录

  11 mins to read  

没爬过自己学校教务网站怎么能说自己会敲爬虫 : )

在此记录模拟登录cumt教务系统

Demo

正文

教务系统网址

和许多学校相同,都是正方教务系统(ummm正方和煎蛋难兄难弟)

查看源代码

可以看到由五个js进行登录加密,为RSA加密,不了解rsa的看这里:RSA加密

提交表单

post的数据包括csrf令牌以及明文的yhm(即学号,我随便敲的),和base64加密的mm(提交了两次),即密码

csrftoken

用来防止跨站请求伪造

源代码中搜索,找到随机生成的token表单value

登录加密

查看login.js

找到获取公钥私钥的地址

cookies问题

使用requests库的requests.session()保持会话

登录逻辑:从登录页面获取csrftoken,请求login_getpublickey.html提交时间参数获取rsa密钥,对获取到的密钥base64解码,用密钥对登录密码进行rsa加密,对密文再进行base64编码,最后post


rsa加密是最麻烦的地方

看了教务系统的base64编码js,发现编码方式为hex串。由于使用python标准库中的base64会将hex串转为字节,而这里的RSA密钥则是需要完整的hex字符串,例如标准库中a0 => YTA=,而我需要a0 => oA==,即将a0看作一个字节的hex值进行编码。

故写了个base64 => hex的算法,其实图方便可以直接把base64.js改写为python版,但我自己写的原因是:…那个算法我没看懂

class HB64(object):

    b64byte = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    b64cpt = "="

    def hex2b64(self, string):
        result = ""
        ptr = 0
        b1 = int("111111000000000000000000", 2)
        b2 = int("000000111111000000000000", 2)
        b3 = int("000000000000111111000000", 2)
        b4 = int("000000000000000000111111", 2)
        lenth = len(string)
        while ptr+6 <= lenth:
            temp = int(string[ptr:ptr+6], 16)
            result += self.b64byte[(temp & b1) >> 18] 
            result += self.b64byte[(temp & b2) >> 12]
            result += self.b64byte[(temp & b3) >> 6]
            result += self.b64byte[temp & b4]
            ptr += 6
        if lenth-ptr == 4:
            temp = int(string[ptr:ptr+4], 16) << 2
            result += self.b64byte[(temp & b2) >> 12]
            result += self.b64byte[(temp & b3) >> 6]
            result += self.b64byte[temp & b4]
            result += self.b64cpt
        elif lenth-ptr == 2:
            temp = int(string[ptr:ptr+2], 16) << 4
            result += self.b64byte[(temp & b3) >> 6]
            result += self.b64byte[temp & b4]
            result += self.b64cpt * 2
        elif lenth-ptr == 0:
            pass
        else:
            raise Exception
        return result

    def b642hex(self, string):
        result = ""
        ptr = 0
        lenth = len(string)
        b1 = int("111111110000000000000000", 2)
        b2 = int("000000001111111100000000", 2)
        b3 = int("000000000000000011111111", 2)
        while ptr+8 <= lenth:
                temp = string[ptr:ptr+4]
                temp_result = 0
                for cell in range(4):
                    temp_result += self.b64byte.index(temp[cell]) << (6 * (3 - cell))
                r1 = hex((temp_result & b1) >> 16)[2:]
                r2 = hex((temp_result & b2) >> 8)[2:]
                r3 = hex(temp_result & b3)[2:]
                if len(r1) == 1:
                    r1 = '0' + r1
                if len(r2) == 1:
                    r2 = '0' + r2
                if len(r3) == 1:
                    r3 = '0' + r3
                result += r1
                result += r2
                result += r3
                ptr += 4
        if string[-1]=="=" and string[-2]=="=":
            temp = string[ptr:ptr+2]
            temp_result = 0
            temp_result += self.b64byte.index(temp[0]) << 18
            temp_result += self.b64byte.index(temp[1] >> 4) << 12
            r1 = hex((temp_result & b1) >> 16)[2:]
            r2 = hex((temp_result & b2) >> 8)[2:]
            if len(r1) == 1:
                r1 = '0' + r1
            if len(r2) == 1:
                r2 = '0' + r2
            result += r1
            result += r2

        elif string[-1]=="=":
            temp = string[ptr:ptr+3]
            temp_result = 0
            for cell in range(2):
                temp_result += self.b64byte.index(temp[cell]) << (6 * (3 - cell))
            temp_result += self.b64byte.index(temp[2] >> 2) << 6
            r1 = hex((temp_result & b1) >> 16)[2:]
            r2 = hex((temp_result & b2) >> 8)[2:]
            r3 = hex(temp_result & b3)[2:]
            if len(r1) == 1:
                r1 = '0' + r1
            if len(r2) == 1:
                r2 = '0' + r2
            if len(r3) == 1:
                r3 = '0' + r3
            result += r1
            result += r2
            result += r3
        elif "=" not in string:
            temp = string[ptr:ptr+4]
            temp_result = 0
            for cell in range(4):
                temp_result += self.b64byte.index(temp[cell]) << (6 * (3 - cell))
            r1 = hex((temp_result & b1) >> 16)[2:]
            r2 = hex((temp_result & b2) >> 8)[2:]
            r3 = hex(temp_result & b3)[2:]
            if len(r1) == 1:
                r1 = '0' + r1
            if len(r2) == 1:
                r2 = '0' + r2
            if len(r3) == 1:
                r3 = '0' + r3
            result += r1
            result += r2
            result += r3
        else:
            raise Exception
        return result

RSA加密是JS中的jsbn进行大数字运算的特定加密,有setPublicKey方法,一样不同于python标准库,见此

参考stackoverflow文章戳我,用了github上别人写的JS原生RSA加密的python版程序

代码

class httpmthd():
    sessions = requests.session()
    time = int(time.time())

    def __init__(self,user,passwd):                       
        self.user = str(user).encode("utf8").decode("utf8")
        self.passwd = str(passwd).encode("utf8").decode("utf8")

    def get_public(self):                       #获得rsa公钥json保存在pub字典中
        url = 'http://202.119.206.62/jwglxt/xtgl/login_getPublicKey.html?time='+str(self.time)
        r = self.sessions.get(url)
        self.pub = r.json()

    def get_csrftoken(self):                    #提取token
        url = 'http://202.119.206.62/jwglxt/xtgl/login_slogin.html?language=zh_CN&_t='+str(self.time)
        r = self.sessions.get(url)
        r.encoding = r.apparent_encoding
        soup = BeautifulSoup(r.text,'html.parser')
        self.token = soup.find('input',attrs={'id':'csrftoken'}).attrs['value']

    def process_public(self,str):               #处理密码,rsa加密
        a = HB64()
        self.exponent = a.b642hex(self.pub['exponent'])           #将json中的base64加密公钥解密
        self.modulus = a.b642hex(self.pub['modulus'])
        rsa = RSAJS.RSAKey()
        rsa.setPublic(self.modulus, self.exponent)                          #rsa加密
        cry_data = rsa.encrypt(str)
        return a.hex2b64(cry_data)                                #加密后的数据进行base64加密

    def post_data(self):                        #post数据
        try:
            url = 'http://202.119.206.62/jwglxt/xtgl/login_slogin.html'
            header = {
                'Accept':'text/html,application/xhtml+xm…plication/xml;q=0.9,*/*;q=0.8',	
                'Accept-Encoding':'gzip, deflate',
                'Accept-Language':'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
                'Connection':'keep-alive',
                'Content-Length':'470',
                'Content-Type':'application/x-www-form-urlencoded',
                'Host':'202.119.206.62',
                'Referer':'http://202.119.206.62/jwglxt/xtgl/login_slogin.html?language=zh_CN&_t='+str(self.time),
                'Upgrade-Insecure-Requests':'1',
                'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0',	
            }
            self.header = header 
            data = [
                ('csrftoken',self.token),
                ('mm',self.process_public(self.passwd)),             #对密码进行加密
                ('mm',self.process_public(self.passwd)),             #post的data数据有两个相同mm字段
                ('yhm',self.user)
            ]
            self.req = self.sessions.post(url,headers = header,data = data)
            ppot = r'用户名或密码不正确'
            if re.findall(ppot,self.req.text):
                print('用户名或密码错误,请查验..')
                time.sleep(2)
                exit()
        except:
            print('登录失败,请检查网络配置或检查账号密码...')
            time.sleep(1)
            exit()

放上代码地址,使用时导入login.py,调用httpmthd类即可

github地址

此处的requests库会有一个编码问题,按报错把库文件代码里某处的’latin1’改为’utf-8’就可以解决

模拟登录后顺便做了成绩获取

打算有时间了敲一个多线程暴力抢课脚本,但是很狗的是,敲完程序测试的机会就只有学期末抢课那几天,这学期敲好调试好要等到过一学期才能拿来用 ( :

好了不管这些,反正模拟登录成功后就可以为所欲为了


selenium脚本

除了利用RSA加密密码外,还可以使用selenium直接提交表单

缺点是button的click()有几率失效,网上有解决办法,不多讨论

P.s在此强烈谴责selenium喜新厌旧的行为!!!居然抛弃了PhantomJS转投火狐和chrome的无头版本,然而火狐无头实例化耗时比phantom多了不少

而且既然不支持phantom,为啥库文件里还是有phantom的包,直到运行的时候才报错说phantom已经过时了,请使用火狐或谷歌…莫名其妙.jpg


更多思路

还可以node.js本地运行js加密,或者提交到在线rsa加密网站

反正方法有很多,没有验证码也可以说对爬虫很友好了


第一篇博客文章end:)

2018.3.28