初见 Chrome Headless 第二弹
0x00 承接上篇
上篇只是研究了 chrome headless 的启动以及部署,顺便跑起来个 demo,距离放到实际的漏扫检测中还是有点难度的,和 wilson 师傅做了一些奇怪的尝试,记录一下。
0x01 成为一个 server 吧
由于是借用的 CDP 与 chrome 进行的通信,所以想到利用 Express 框架搞个 server,不停的给他post 数据,让他进行检测,于是第一版代码很快就写出来了,这个就不贴了,没什么用。测试的时候遇到一个问题,如果我们的漏扫(Python的)快速的向他 post 了一把 URL,比如5个,chrome 就崩了。
一开始以为是 CDP 的问题或者是 Express 的问题,仔细研究发现,其实是我们的姿势不对。我们的第一版代码中,只开启了一个 CDP的实例,也就是只开启了一个 chrome 的 tab 页,(没想到 chrome 的 headless 模式竟然如此完备)不停的 POST 之后,chrome 只在一个 tab 页进行操作,所以就出现了类似竞争的问题,导致出错。
由于 node 水平实在有限,又不想用自己不熟悉的语言去搞这件事情,于是和 wilson 师傅商量我们用 Python 写个和 chrome 交互的 API,这样扫描器调用起来也会很方便。
0x02 踏上蛋疼的旅程
翻了一大把资料+读 CDP 源码之后发现,与 Chrome 交互的过程实际上是与 Chrome DevTools 进行交互,走的 websocket。也就是说,google 并没有搞出一套新的通信协议来,用的还是 Chrome DevTools 的协议。不仅仅可以操控 headless,非 headless 也可以借用这一套协议进行交互。
后来不记得在哪里翻出来一个 python 版的这套协议的实现,我们自己的实现其实也参考了这个项目:https://github.com/minektur/chrome_remote_shell
简单说一下这套协议吧,这套协议通过 websocket 进行通信,发送和返回的内容都是 json 格式。发送的格式大概是这样:
{
"id": id,
"method": command,
"params": params
}
换成一个实际的例子可能是这样:
{
"id": 1,
"method: "Page.enable",
"params": {}
}
{
"id": 2,
"method": "Page.navigate",
"params": {"url": "https://www.github.com"}
}
在 CDP 的项目首页中有链接给到了所有的协议类型:https://chromedevtools.github.io/debugger-protocol-viewer/tot/ 很多很全很厉害。这样一来就简单很多,我们自己用 Python 连接 websocket 发 json 就可以了。
0x03 距离终点还很远
然而事情并没有我和 wilson 师傅想的这么简单。我们遇到了两个问题:
- 上次 node 代码里的事件监听怎么搞,例如:Page.javascriptDialogOpening 等。
- 怎么知道去连哪个 websocket 地址。
我们一个一个来解决,先来解决第二个问题,就是 websocket 地址的问题。
我们通过 CDP 的源码,发现了几个关键的 URI,分别是:
- http://localhost:9222/json
- http://localhost:9222/json/new
- http://localhost:9222/json/close/tab_id
其中第一个 URL 是获取当前所有打开的 TAB 页,第二个是新建一个 TAB 页,第三个是根据 TAB 页的 id 关闭这个 TAB 页。
当我们请求第一个 URL 时,返回的内容大概如下:
[
{
"description": "",
"id": "c33a4799-13e0-4b6a-b636-fd717c32c941",
"title": "a.html",
"type": "page",
"url": "http://x.x.x.x/a.html"
},
{
"description": "",
"devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/1adf9b16-5cca-483e-874a-2a53f4b131ca",
"id": "1adf9b16-5cca-483e-874a-2a53f4b131ca",
"title": "about:blank",
"type": "page",
"url": "about:blank",
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/1adf9b16-5cca-483e-874a-2a53f4b131ca"
}
]
这里面可以拿到每个 TAB 页的详细信息。第二个新建 TAB 页访问之后,也会返回新 TAB 页的信息。其中就有一个很重要的字段:webSocketDebuggerUrl
,这个就是我们要拿的 websocket 的地址。
另外一个问题,怎么处理监听事件的问题。一开始我还抓websocket 包,发现并没发送监听事件相关的内容,就陷入了无尽的思索。然后无意间发现,当我们发送完 Page.navigate 这个命令后,除了返回一个 id 相等的 json 之外,还会发送回来一堆东西。一开始代码中只 recv 了一次,所以一直没有看到这些,其中就有所有触发的事件,大概是这个样子:
{"id":2,"result":{"frameId":"33320.1"}}
{"method":"Page.frameNavigated","params":{"frame":{"id":"33320.1","loaderId":"33320.2","url":"http://x.x.x.x/a.html","securityOrigin":"http://x.x.x.x","mimeType":"text/html"}}}
{"method":"Page.javascriptDialogOpening","params":{"message":"9527","type":"alert"}}
{"method":"Page.javascriptDialogClosed","params":{"result":true}}
{"method":"Page.loadEventFired","params":{"timestamp":131319.852874}}
{"method":"Page.frameStoppedLoading","params":{"frameId":"33320.1"}}
{"method":"Page.domContentEventFired","params":{"timestamp":131319.853225}}
这样只要不停的接受返回,就可以拿到触发的所有事件了。但是在我们自己封 Python 包的时候,出现了问题。我们使用的是 websocket-client 库,这个 recv() 如果没有消息时,会 block 住,只好采取曲线救国策略。
0x04 封个包吧
我的代码中封了一个 ChromeHeadlessInterface 类,每一个类的实例代表一个 tab 页。主要实现了open_tab
, close_tab
, add_event_listener
, send_command
, recv
方法。思路很简单,就是发一下,收一下,检查收到的消息中有没有触发用户监听的事件,有的话调用用户提供的 callback。因为 recv 会 block,我这里就提供了两种停止接收数据的方式,分别是通过字符串和 id 进行停止接收。
根据字符串停止比较好理解,就是出现了某个消息,我就认为这次通信完成了,可以继续发送下一个命令了。id 的方式就是比较这次返回的内容中是否有刚刚发送过去的 id,如果有的话,就认为可以停止了。大概就这样调用:
from ChromeHeadlessInterface import ChromeHeadlessInterface
def hook(params):
print("detected alert!")
def hook2(params):
print("detected alert! again!!")
chi = ChromeHeadlessInterface()
chi.add_event_listener("Page.javascriptDialogOpening", hook)
chi.add_event_listener("Page.javascriptDialogOpening", hook2)
chi.send_command("Page.enable")
result = chi.recv()
print(result)
chi.send_command("Page.navigate", {"url": "http://x.x.x.x/a.html"})
result = chi.recv("Page.domContentEventFired")
print(result)
chi.close()
wilson 师傅看完我的代码表示太辣鸡了,于是自己也封了一个,思路大概就是,一边疯狂的发命令,另一边不停的收结果,每收到一条结果就匹配一下用户监听的事件和 id,进行不同的操作。两份代码都放到了 github 上,大家可以参考下。地址:https://github.com/wilson9x1/ChromeHeadlessInterface
PS:如果不想自己折腾,可以看看selenium+chrome headless。链接:https://duo.com/blog/driving-headless-chrome-with-python,至于我们为啥要自己造个轮子,只能说对于我们的场景以及需求以及漏扫,自己封个轮子比直接调用这个东西更好用吧。
[...]https://lightless.me/archives/chrome-headless-second.html[...]
[...]https://lightless.me/archives/chrome-headless-second.html[...]
你好,我最近也在看相关的内容,发现资料很少,看到您也在学习相关内容,想请教个问题。
我发现这个用websocket处理好像是异步的,那如果我想在网页发送某个静态或者动态请求前对这个请求处理怎么操作,用websocket岂不是没法对请求进行修改或block,我找API文档试了半天仍然没有结果。
Network.enableRequestInterception这个方法我试验了一下只会返回Document页面的消息,不会返回css,js,ajax等请求,也不会阻塞请求。就算返回了,那我该怎样才能处理这些请求,就算在我处理之前,这些资源可能就已经被浏览器自动处理完成了。
谢谢。
这套协议完全是DevTools的协议,也就是说,这里只能做在开发者调试工具中做的事情,除此之外其他的额外功能应该都要自己实现了。
我注意到官方的api文档有句话 https://chromedevtools.github.io/devtools-protocol/ :
We currently do not support multiple clients connected to the protocol simultaneously. This includes opening DevTools while another client is connected. On the bug tracker, crbug.com/129539 follows the issue; you can star it for email updates.
我的理解不管chrom开了多少tab,同时间只能一个ws client接入?
你是怎么处理这种情况的?
并没有遇到这种问题,我想文档的意思可能是『多个 client 无法同时连入同一个 tab 中的 devtools』?
:)