难度: ★☆☆☆☆ 1星
一、目标
看到群里有人发了一个网址:
http://gcxm.hunanjs.gov.cn/dataservice.html
这是个查询的页面,打开是这个样子的:
如果想查询东西,单击“搜索”按钮的时候就会弹出一个滑块验证码:
本次要破解的就是这个滑块验证码。
二、分析
首先确定一下这个滑块是如何提交的,打开开发者工具,切换到Network清空无关请求,然后故意拖动失败一次,观察到页面验证码图片刷新,同时捕捉到了四个请求:
先看第一个请求,这个请求的url是:
http://gcxm.hunanjs.gov.cn/AjaxHandler/PersonHandler.ashx?method=GetListPage&type=1&corptype_1=&corpname_1=&licensenum_1=&Province_1=430000&City_1=&county_1=&persontype=&persontype_2=&personname_2=&idcard_2=&certnum_2=&corpname_2=&prjname_3=&corpname_3=&prjtype_3=&cityname_3=&year_4=2020&jidu_4=3&corpname_4=&corpname_5=&corpcode_5=&legalman_5=&cityname_5=&SafeNum_6=&corpname_6=&corpname_7=&piciname_7=&corptype_7=&corpname_8=&corpcode_8=&legalman_8=&cityname_8=&pageSize=30&pageIndex=1&xypjcorptype=3&moveX=167&verifyid=87c6f516515346a69f4eec019b43faa7
这样不是很好阅读,还是看Chrome中解析的query string:
最上面的method表明这是个查询列表的请求,然后是一些过滤条件忽略,最下面这两个参数是和验证码相关的,经过几次试验后证明,moveX是我们拖动的距离,verifyid应该是表示本次验证的id,是每次刷新验证码的时候从服务器返回的。
然后看这个接口的响应:
没什么好说的,验证失败...
然后继续看刚刚捉到的第二个请求:
http://gcxm.hunanjs.gov.cn/AjaxHandler/PersonHandler.ashx?method=GetVerifyImg
明显这是个获取新的验证码的请求,看下它的响应:
是个数组类型,数组中有4个元素,数组中的前两个元素看上去像是base64编码的图片,复制出来加上“data:image/png;base64,”前缀就能在chrome的地址栏中直接查看:
然后是第二张图:
这两张图片和页面上刚刚刷新的验证码是一致的:
然后是数组中的第3个元素,猜测这个应该就是上一个提交请求里的verifyid,要验证这点只需要再提交一次失败的请求就可以了:
然后对比下这个提交请求里的verifyid,发现是一样的。
但是数组中的第四个元素是什么呢,猜测也许是滑块相对于背景图的垂直位置,但是没有证据,我们为这个请求打一个xhr断点:
http://gcxm.hunanjs.gov.cn/AjaxHandler/PersonHandler.ashx?method=GetVerifyImg
然后刷新页面,看到进入了断点,然后格式化代码:
观察调用栈,发现有个叫getVerifyImg的栈帧,点击跳到那个栈帧,然后看到了接口success后的回调方法:
接下来就是阅读一下相关的代码,data是接口服务器响应的数据,然后调用了一个_this.createVerifyContent方法并把data传了进入:
然后跟进入这个方法:
这个方法好长,都是拼接html的:
我们只关心data[3]是如何被使用的,只有一个地方使用到了data[3]:
这是将将其作为一个id为cutImg的元素的top属性,切换到Element,搜索定位这个元素:
然后双击top: 24,进入编辑模式,键盘上下键调整发现页面上的滑块位置随之上下移动,确定就是滑块的垂直距离。
接下来就是如何获取背景图上缺口的位置,我们注意到背景图上的缺口的颜色总是白色的,因此我们只需要扫描背景图,检测垂直位置上出现了连续n个白色像素即认为是缺口所在的位置就可以了,不过这里有个坑,我开始在实现的时候以为背景图是纯白色的,因为我的肉眼看上去那就是白僧僧的...然后发现程序的识别率不高,然后我下载了一张图片拖到ps中看了一下:
这个看上去老实巴交的白色缺口实际上并不是纯色的,还好我有编码实践的习惯不然看一眼就过可能就发现不了这个坏坏的白块了,不是纯白色也没关系,至少是接近的,因此在判断颜色的时候取个近似的cutoff就可以了,然后是缺口的高度,拿参考线量了一下发现是40px,为了保险认为垂直方向上连续出现30个近似白色的像素就认为是发现了缺口的位置。
好了,问题分析的差不多了,接下来就是编码实现。
三、编码实现
#!/usr/bin/env python3 # encoding: utf-8 """ @author: CC11001100 """ import base64 import io import numpy import requests from PIL import Image session = requests.session() def crawl(): image_matrix, top, verify_id = get_verify_img() # 滑块缺口的位置 lack_x = find_lack(image_matrix, top) print(lack_x) submit(verify_id, lack_x) def get_verify_img(): url = "http://gcxm.hunanjs.gov.cn/AjaxHandler/PersonHandler.ashx?method=GetVerifyImg" response = session.get(url) # 是这么用的吗?我很懵逼... session.cookies.update(response.cookies) response_json = response.json() # 滑块背景图片 image_base64 = response_json[1] image_bytes = base64.b64decode(image_base64) image = Image.open(io.BytesIO(image_bytes)) image.save("a.png") image_matrix = numpy.asarray(image) # 本次验证的id verify_id = response_json[2] # 滑块距离顶部的距离 top = int(response_json[3]) print(response_json) return image_matrix, top, verify_id def submit(verify_id, lack_x): url = "http://gcxm.hunanjs.gov.cn/AjaxHandler/PersonHandler.ashx" params = { "method": "GetListPage", "type": 1, "corptype_1": 2, "corpname_1": "", "licensenum_1": "", "Province_1": 430000, "City_1": "", "county_1": "", "persontype": "", "persontype_2": "", "personname_2": "", "idcard_2": "", "certnum_2": "", "corpname_2": "", "prjname_3": "", "corpname_3": "", "prjtype_3": "", "cityname_3": "", "year_4": 2020, "jidu_4": 3, "corpname_4": "", "corpname_5": "", "corpcode_5": "", "legalman_5": "", "cityname_5": "", "SafeNum_6": "", "corpname_6": "", "corpname_7": "", "piciname_7": "", "corptype_7": "", "corpname_8": "", "corpcode_8": "", "legalman_8": "", "cityname_8": "", "pageSize": 30, "pageIndex": 1, "xypjcorptype": 3, "moveX": lack_x, "verifyid": verify_id } headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36", "X-Requested-With": "XMLHttpRequest", "Referer": "http://gcxm.hunanjs.gov.cn/dataservice.html" } r = session.get(url, params=params, headers=headers) print(r.text) def find_lack(image_matrix, top): """ 拿到缺块所在的列的x坐标 :param image_matrix: :param top: :return: """ for i in range(0, len(image_matrix[0])): count = 0 for j in range(top, len(image_matrix)): point = image_matrix[j][i] cutoff = 230 if point[0] >= cutoff and point[1] >= cutoff and point[2] >= cutoff: count += 1 if count >= 30: return i return -1 if __name__ == "__main__": crawl()
程序运行效果:
本文相关代码内容托管至:
https://github.com/CC11001100/misc-crawler-public/tree/master/003-captcha/01-001-gcxm.hunanjs.gov.cn
请注意爬虫文章具有时效性,本文写于2020-11-14日。