好久都没有水文章了,记得上一次水的文章还是在上一次(废话文学),当然记得:《JS逆向|某教学CMS(x可思)请求体加密分析 》, BUG-Fly (3xittec.cn)还是在去年12月,看来是自己懒了!
0x00 缘起
如题所见,今天水的这篇文章就是解决在 Chrome Headless 模式下如何 优雅 地去触发网站的 favicon 请求。看到这里可能会产生疑问:Chrome 在 Headless 模式下不会触发网站的 favicon 请求吗?其实当我第一次听到这个问题,也和正在阅读文章的你是同样的问号脸!答案:不会触发。
这个问题的发现,还是组里的一位做资产识别的前辈发现的,当时我一脸疑惑,然后亲自做了实验发现确实如此,Chrome 在 Headless 模式下确实不会触发网站的 favicon.ico 请求。
0x01 相识
为了探究是实现和 Chrome 通信的框架问题,还是 Chrome 在 Headless 模式下的默认渲染行为。我对市面上的网络安全爬虫工具分别在 Chrome Headful 模式和 Chrome Headless 模式下做了实验。
Chrome Headless 模式
Rad
Rad 是长亭科技的著名的扫描器 Xray 集成的网络安全爬虫工具,其采用 GO 开发,采用的通信框架是rod , 是一个直接基于 DevTools Protocol 高级驱动程序。 使用 Rad 对 github 进行了测试,测试截图如下:
如图所示,在输出的结果中搜索未见到“favicon”字样。
HAWK-X
HAWK-X 是绿盟科技下一代智能爬虫引擎。其采用 GO 开发,是一款基于事件驱动的 Web 2.0 启发式浏览器爬虫,拥有业界领先的登录扫描方式、分布式能力等诸多特性。已经服务于绿盟科技的多款工具和产品,如 WVSS/RSAS/EZ 等。与 Rad 一样使用的通信框架也是 rod ,使用 HAWK-X 对 github 进行了测试,测试结果如下图:
如图所示,在输出的结果中搜索未见到“favicon”字样。
Playwright
Playwright(通信框架)是微软在 2020 年初开源的新一代自动化测试工具,它的功能类似于 Selenium、Puppeteer 等,都可以驱动浏览器进行各种自动化操作。使用 Playwright 对 github 进行测试,测试结果如下图:
如图所示,在输出的结果中搜索未见到“favicon”字样。
Chrome Headful 模式
Rad
禁用 Rad 的 headless 模式,再次对 github 进行测试,测试结果如下图:
从输出结果中,发现了 “favicon”字样。
HAWK-X
禁用 HAWK-X 的 headless 模式,再次对 github 进行测试,测试结果如下图:
从输出结果中,发现了 “favicon”字样。
Playwright
禁用 Play的 headless 模式,再次对 github 进行测试,测试结果如下图:
哦,有着不一样的发现:PlayWright 的 headful 模式下竟然也没有 favicon 相关的请求。按道理说在 headful 模式下,应该会有 favicon 相关请求的,在 F12 Network选项卡中也能查看得到,此时敏锐的嗅觉告诉我,此事必有蹊跷!
经过上述实验,可以得到以下实验结果表格:
工具\框架 | Headless | Headful |
---|---|---|
Rad(rod) | 未触发 | 触发 |
HAWK-X(rod) | 未触发 | 触发 |
PlayWright | 未触发 | 未触发 |
看来 PlayWright 的框架本身另有玄机!
0x02 相知
从上述的实验结果,是否可以就此下定结论:无法触发 favicon 的请求,是因为 Chrome 在 Headeless 模式下的渲染行为所为,而和框架本身无关?
Playwright 反常结果的求证
PlayWright 本身也是基于 CDP 协议实现的,结果应与另外两者相同才是,这不免产生怀疑:Playwright 框架本身对 favicon 相关请求做了过滤。
开始着手在 PlayWright 的 issue 里寻找线索,搜索 favicon 关键字,果然有东西:
[BUG] page.on(‘request’) is not capturing favicon.ico URI #7493 这条 issue 吸引了我的注意,点进去在对话中发现:
简单且核心地翻译一下:我们目前有意的过滤掉 favicon 的请求。
本节开始的怀疑有了答案:PlayWright 实在框架层面对 favicon 相关请求做了过滤。
不过为什么会对 favicon 请求做过滤呢?继续往下探索,发现了下面这条 pr:
feat(network): ignore favicon requests – these are too unpredictable #533
理由:favicon 相关的请求是不可预测的。
确实如此,有些站点的 favicon 并非是 “host:port/favicon.ico”,亦或是存放在其他路径下或者是其他image类型的,例如我的站就是 .png 结尾的。这确实会给程序带来不稳定因素。
现在一切都已经真相大白,此时我们可以下定结论就是:Chrome 在 Headeless 模式下不会去触发 favicon 的请求。这是 Chrome Headless 的默认渲染行为。
0x03 相爱
经过前文的分析:Chrome 在 Headeless 模式下不会去触发 favicon 的请求。这是 Chrome Headless 的默认渲染行为。 已经明白了原因,那就去解决它!
Chrome Flags
起初打算尝试通过一些 Chrome 的启动参数,看看能不能有收获,但是当我看到全部参数的时候我陷入了沉思:
我看了一下这些参数至少有百余个之多,这一个个试得多麻烦,这条 Pass。
工匠精神——造轮子
打算在 Headless 模式下直接使用,Python/GO 的HTTP请求库,拼接相应 favicon 的连接,直接发起请求。但是我觉得这样不够优雅,既然都已经选择了 Chrome Headless 为何还要在 Chrome Headless 之外额外的发起请求呢!实在是不够优雅。既然前边这几个通信框架都是基于 CDP 协议实现的那不为何自己造轮子,实现一个自己的 TriggerFavicon()
的 API呢。
思路很直接也很粗暴:既然 Chrome 在 Headless 模式下选择不去渲染触发 favicon 请求,那我们就“强迫”它把它应该做的事给做了。
Rod 版
// golang
func (b *Browser) IsHeadless() bool {
res, err := proto.BrowserGetBrowserCommandLine{}.Call(b)
utils.E(err)
for _, v := range res.Arguments {
if strings.Contains(v, "headless") {
return true
}
}
return false
}
func (p *Page) TriggerFavicon() error {
if !p.browser.IsHeadless() {
return errors.New("Browser is headful")
}
proto.PageSetLifecycleEventsEnabled{Enabled: true}.Call(p)
wait := p.EachEvent(func(e *proto.PageLifecycleEvent) bool {
return e.Name == "DOMContentLoaded"
})
wait()
js := `() => {
faviconElement = document.querySelector("link[rel~=icon]");
href = (faviconElement && faviconElement.href) || "/favicon.ico";
faviconUrl = new URL(href,window.location).toString();
xhr = new XMLHttpRequest();
xhr.open("GET",faviconUrl);
xhr.send();
xhr.addEventListener("readystatechange",function () {
if (xhr.readyState === 4) {
if (xhr.status>=200 && xhr.status <= 300) {
return faviconUrl;
}
}
});
}`
_,err:= p.Evaluate(Eval(js).ByUser())
if err != nil {
return err
}
p.unsetJSCtxID()
return nil
}
代码逻辑很简单:
实现了两个方法:
IsHeadless
:用于判断吃否处于 Headless 模式下。TriggerFavicon
:判断处于 Headless 模式下,在 Page DOM 渲染完成后注入触发favicon 请求的 JS
PlayWright 版
# Python
async def trigger_favicon(page: Page):
script = """() => {
faviconElement = document.querySelector("link[rel~=icon]");
href = (faviconElement && faviconElement.href) || "/favicon.ico";
faviconUrl = new URL(href,window.location).toString();
xhr = new XMLHttpRequest();
xhr.open("GET",faviconUrl);
xhr.send();
xhr.addEventListener("readystatechange",function () {
if (xhr.readyState === 4) {
if (xhr.status>=200 && xhr.status <= 300) {
return faviconUrl;
}
}
});
}"""
page.on("domcontentloaded", await page.evaluate(script))
return True
代码逻辑和 Rod 版的大差不差,唯独少了一个 IsHeadless
的方法,这是因为 PlayWright 是否是 Headless 模式下都不会拿到 favicon 的请求,所以也就没有必要去实现了,正因如此,这个 API 在 PlayWright 的使用前提是在 CDPSession
模式下:
page = await browser.new_page()
client = await page.context.new_cdp_session(page)
await client.send("Fetch.enable")
await page.route("**/*", lambda route: resp_route(route, output_list, True))
await page.goto("https://github.com")
await trigger_favicon(page)
0x04 总结
上述两个版本都经过实测,稳定触发。当然我觉得这还不是最优雅的,最优雅的还是通过 Chrome 自身去解决这个事情,例如,启动参数(Flags)或者其他方法。如果有对 Chrome 有深入研究的朋友,知道其它好的解决办法,欢迎在评论区留言,探讨交流。
评论 (0)