扫描器里的动态爬虫的一些实践(一)

0x00 扫描器里的爬虫

目标是什么

显然,用requests当爬虫,bs4解析,对现在前端是不行的,动态爬虫越来越重要

实际上我到现在还没懂猪猪侠的启发式爬虫是什么意思,叫做动态爬虫更好吧,其实还是把静态页面分析,改成动态操作js,增加加载,模拟点击和表单自动填写,然后对各种个例的特殊情况做分析,改善js hook的规则

最终目的是hook所有网络请求的接口,爬取更多的CGI请求

怎么hook

  • 期望在页面加载之前注入一段js代码hook、备份各种未被污染的函数
  • 期望在页面加载之后注入一段js代码遍历各个元素、触发各种事件、劫取各种CGI请求和返回

我们要学习的一些基础

BOM(Browser Object Model)和DOM(Document Object Model)

可以看这篇博客BOM和DOM详解

简单来说就是DOM就是html标签解析的各种节点并被js加载成为的对象,就是处理HTML标签的对象

BOM是主要处理浏览器窗口的框架,就是打开chrome开发这工具,console里的window等对象

CDP(Chrome DevTools Protocol),因为我们使用Chrome Headless作为模拟浏览器进行爬取工作

CDP可以看文档,可以看作Chrome等远程调试接口

Puppeteer是Chrome官方的Node库,提供高级API来通过DevTools控制Chromium或Chrome,默认用headless模式运行

主要需要攻克的技术点

  • 怎么定义页面加载的前与后
  • 怎么注入js
  • 注入js执行哪些功能

我们使用到一些编程技术

主要是操作模拟浏览器的Chrome Headless,又因为我写扫描器的多用Python开发,而且node不会写,所以需要了解Python怎么操作chrome headless

我最开始接触的是pyppeteer因为猪猪侠在先知的PPTWEB2.0启发式爬虫实战

后来慢慢了解CDP,就慢慢学会直接操作CDP,就了解到了pychrome,缺点是两个都不是官方的,维护没有保证,优点是都可以用

重点还是要了解CDP的接口

0x01 页面加载前后的区分

启动一下CDP

我们使用pyppeteer的时候它会打开浏览器,但是一般我们在服务器上部署headless的chrome

$ google-chrome --remote-debugging-port=9222 --headless --disable-gpu

pychrome提供了docker启动的方式

$ docker pull fate0/headless-chrome
$ docker run -it --rm --cap-add=SYS_ADMIN -p9222:9222 fate0/headless-chrome

DevTools listening on ws://0.0.0.0:9222/devtools/browser/30261dd5-e97c-47d1-b3e1-0195bdff536c

启动后会出现上面的listening on,这个在pyppeteer上有用

页面加载的一些阶段

这部分主要参考了爬虫 JavaScript 篇

我们首先看看页面加载有几个阶段

先ping一下baidu

import pychrome
import pychrome.exceptions


# 监听事件生命周期的阶段
def lifecycle_event(**kwargs):
    print("{}: {}".format(kwargs['timestamp'], kwargs['name']))


# 打开CDP浏览器
browser = pychrome.Browser(url='http://127.0.0.1:9222')
# 打开一个新标签
tab = browser.new_tab()
# 设置页面监听事件生命周期的阶段
tab.Page.lifecycleEvent = lifecycle_event
# 使用tab和page
tab.start()
tab.Page.enable()
try:
    # 设置监听器可用
    tab.Page.setLifecycleEventsEnabled(enabled=True)
except pychrome.exceptions.CallMethodException:
    pass
# 定向到baidu
tab.Page.navigate(url="https://www.baidu.com")
tab.wait(60)

查看输出

241629.600416: commit
241629.600472: DOMContentLoaded
241629.60059: load
241629.600731: networkAlmostIdle
241629.600731: networkIdle
241629.600731: networkAlmostIdle
241629.600731: networkIdle
241669.66635: load
241669.666721: DOMContentLoaded
241669.666743: networkAlmostIdle
241669.666743: networkIdle

commit应该是发出请求,这里不管,我们看看剩下4个是什么意思

  • DOMContentLoaded:一般表示DOM和CSSOM均准备就绪的时间点
  • networkAlmostIdle:当前网络连接数少于2后触发
  • networkIdle:当前没有网络连接触发
  • load:网页所有资源载入后触发,浏览器上加载转环停止旋转

一般情况下时间的触发顺序是:DOMContentLoaded -> networkAlmostIdle -> networkIdle -> load

实际上触发时间不一定,比如上面的baidu,之后networkAlmostIdle一定比networkIdle晚

加载前后如何区分

关键点在于我们怎么判断页面加载完成

我们上看面的几个事件

应该说我第一次没有网络请求的时候就算获取完初始资源了,但是获取的资源如果再需要网络请求资源怎么办

如果load完就算没有资源了,但是如果load太快,还有动态ajax没有完成,完成后会改变网页怎么办

所以如果能按照一般的触发顺序DOMContentLoaded -> networkAlmostIdle -> networkIdle -> load就是好的,load就是结束,否则都很麻烦

看我上面提到的参考文章,说了3种

等待 load,同时设定等待超时时间,load 超时直接注入代码,同时等待 DOMContentLoaded事件

DOMContentLoaded 事件触发,接着等待 networkAlmostIdle,同时设定等待超时时间,超时直接注入代码

networkAlmostIdle 事件触发,接着等待 networkIdle 同时设定等待超时时间,超时直接注入代码

我实在没在pyppeteer种找到timeout还可以继续的接口,就改成了这样

await page.goto('https://www.baidu.com',
                 waitUntil=["networkidle0", "load", "domcontentloaded"],
                 timeout=10000)

不管三七二十一,上面的条件全用上,再加一个timeout的时间就可以了,真的,千万不要纠结

页面加载前:页面创建之后,用户代码执行之前的时间都是页面加载前

页面加载完成:timeout前,且load,domcontentloaded和networkidle都完成

页面加载后:页面加载完成后,开始用户代码执行

0x02 动态爬虫基础

先来体验一下

从这里开始,我们不用pychrome,改用pyppeteer,这一节我们体验一下puppeteer,为下一篇的细节进行准备

我们先来看一下pyppeteer爬取百度URL的简单例子

import asyncio
from pyppeteer.errors import TimeoutError
import pyppeteer


async def main():
    browser = await pyppeteer.connect({
        'browserWSEndpoint': 'ws://127.0.0.1:9222/devtools/browser/30261dd5-e97c-47d1-b3e1-0195bdff536c',
        'ignoreHTTPSErrors': True
    })
    page = await browser.newPage()
    try:
        await page.goto('https://www.baidu.com',
                        waitUntil=["networkidle0", "load", "domcontentloaded"],
                        timeout=10000)
        urls = await page.evaluate('''() => {
            var urls = new Array();
            var atags = document.getElementsByTagName("a");
            for (var i = 0; i < atags.length; i++) {
                if (atags[i].getAttribute("href")) {
                    urls[i] = atags[i].getAttribute("href")
                }
            }
            return urls;
        }
        ''')
        for url in urls:
            print(url)
    except TimeoutError as e:
        print('timeout')


asyncio.get_event_loop().run_until_complete(main())

上面browserWSEndpoint函数就是我上面写CDP运行的时候的输出,有了这个才会和CDP连接,这里要注意一下

这里我们看到page.evaluate注入执行了一些js代码

上面这段代码的输出为

/
javascript:;
javascript:;
javascript:;
javascript:;
/
javascript:;
https://passport.baidu.com/v2/?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2F
https://voice.baidu.com/act/newpneumonia/newpneumonia/?from=osari_pc_1
http://news.baidu.com
https://www.hao123.com
http://map.baidu.com
http://v.baidu.com
http://tieba.baidu.com
http://xueshu.baidu.com
https://passport.baidu.com/v2/?login&tpl=mn&u=http%3A%2F%2Fwww.baidu.com%2F
http://www.baidu.com/gaoji/preferences.html
http://www.baidu.com/more/
//www.baidu.com/s?rtt=1&bsst=1&cl=2&tn=news&word=
http://tieba.baidu.com/f?kw=&fr=wwwt
http://zhidao.baidu.com/q?ct=17&pn=0&tn=ikaslist&rn=10&word=&fr=wwwt
http://music.taihe.com/search?fr=ps&ie=utf-8&key=
http://image.baidu.com/search/index?tn=baiduimage&ps=1&ct=201326592&lm=-1&cl=2&nc=1&ie=utf-8&word=
http://v.baidu.com/v?ct=301989888&rn=20&pn=0&db=0&s=25&ie=utf-8&word=
http://map.baidu.com/m?word=&fr=ps01000
http://wenku.baidu.com/search?word=&lm=0&od=0&ie=utf-8
//www.baidu.com/more/
//www.baidu.com/cache/sethelp/help.html
http://home.baidu.com
http://ir.baidu.com
http://e.baidu.com/?refer=888
http://www.baidu.com/duty/
http://jianyi.baidu.com/
http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=11000002000001

可以自己试试看和requests爬取链接有哪些不同

页面加载前的js注入

更新,很遗憾,经过一段时间pyppeteer没有维护,以下的例子可能已经失效了

测试pyppeteer的page.evaluateOnNewDocument函数始终没有起作用,同样没有起作用还有page.addScriptTag函数

这两个函数我理解API的意思就是用来在页面加载前进行注入代码的,现在实际上不管我怎么测试都没有起效,不能hook js

import asyncio
from pyppeteer import launch


launch_options = {
    # "slowMo": 300,
    "headless": False,
    "devtools": True,
    "ignoreHTTPSErrors": True,
    "args": [
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-gpu",
        "--disable-xss-auditor",
        "--no-sandbox",
    ]}


async def dialog_inject(dialog):
    print('[*] dialog type ' + dialog.type)
    print('[*] dialog message ' + dialog.message)
    print('[*] dialog defaultValue ' + dialog.defaultValue)
    # await dialog.accept()


async def request_inject(request):
    if request.resourceType in ['image', 'media', 'eventsource', 'websocket']:
        await request.abort()
    else:
        await request.continue_()


async def main():

    browser = await launch(launch_options)
    page = await browser.newPage()
    await page.goto('http://localhost:8333/test.html', waitUntil=["networkidle0", "load", "domcontentloaded"])
    await page.evaluateOnNewDocument("""
    window.alert = function(s){
        console.log("Hooked!");
    };
    """)
    await page.waitFor(300000)

asyncio.get_event_loop().run_until_complete(main())

以前这样就可以hook住alert函数,现在已经不能hook了

只要知道页面加载前通过page.evaluateOnNewDocument就可以注入js,hook函数,现在容易出问题,可以注入js,但是不能hook

页面加载后的js注入

import asyncio
from pyppeteer import launch


launch_options = {
    # "slowMo": 300,
    "headless": False,
    "devtools": True,
    "ignoreHTTPSErrors": True,
    "args": [
        "--no-sandbox",
        "--disable-setuid-sandbox",
        "--disable-gpu",
        "--disable-xss-auditor",
        "--no-sandbox",
    ]}


async def dialog_inject(dialog):
    print('[*] dialog type ' + dialog.type)
    print('[*] dialog message ' + dialog.message)
    print('[*] dialog defaultValue ' + dialog.defaultValue)
    # await dialog.accept()


async def request_inject(request):
    if request.resourceType in ['image', 'media', 'eventsource', 'websocket']:
        await request.abort()
    else:
        await request.continue_()


async def main():

    browser = await launch(launch_options)
    page = await browser.newPage()
    await page.goto('http://localhost:8333/test.html', waitUntil=["networkidle0", "load", "domcontentloaded"])
    await page.evaluate("""
    window.alert =function(s){
        console.log("Hooked!");
    };
        alert("123");
    """)
    await page.waitFor(300000)

asyncio.get_event_loop().run_until_complete(main())

这个就好理解了,打开网页后,注入js,这个可以hook成功,console会输出Hooked!

知道了怎么hook,怎么注入js,我们就结束了这一篇,开始下一篇讲细节

坚持原创技术分享,您的支持将鼓励我继续创作!