教你如何编写一个黑阔浏览器扩展程序
借助扩展程序,您可以编写被动和主动漏洞扫描程序。您可以自动执行任务,甚至可以编写垃圾邮件机器人或账户预热机器人。您可以实施某些类型的漏洞利用。你可以收集信息,就像 Wappalyzer 收集技术信息一样。你可以创建包含快速提示的参考书,就像反向 Shell 生成器一样

为 Firefox 浏览器编写一个扩展程序。但需要注意的是,Chrome 和 Firefox API 之间的差异通常并不大,因此几乎每个扩展程序都可以适配到所需的浏览器。无论如何,一切都始于 manifest.json 文件。该文件使浏览器了解如何与分辨率进行交互。该文件的最简单版本如下所示:
JavaScript:复制到剪贴板
{
"manifest_version": 3,
"name": "Passive Hack | .is",
"description": "Extension for passive scanning of web applications. Developed specifically for the article on the .is website",
"version": "0.0.1"
}
不,说真的,这是一个浏览器会“吃掉”的现成扩展程序。它什么都不做。它甚至没有图标,只有名称、版本和文字描述。要将我们的调试扩展程序添加到浏览器,您需要访问 about:debugging#/runtime/this-firefox 并点击“加载临时插件”……之后,它就会出现在临时扩展程序中。
请注意,我们将使用清单的第三个版本,而不是第二个版本。这不仅会影响 JSON 本身的结构,还会影响浏览器与扩展程序的交互方式。第三个版本是 Chrome 和 Firefox 的当前版本。
WP Detect 扩展文件
为了或多或少准确地确定Wordpress的版本,我们需要这样的结构:
- 内容脚本将负责处理 HTML 代码和附加请求,以查找 feed 并根据它确定版本
- 后台脚本,因为内容脚本不知道在浏览器中发送通知的可能性。
- 弹出窗口,其中我们将显示有关版本确定的详细数据
清单文件将如下所示:
JSON:复制到剪贴板
{
"manifest_version": 3,
"name": "WordPress version detector | .is",
"description": "WordPress version detector. Developed specifically for the article on the .is website",
"version": "0.0.1",
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_end"
}
],
"action": {
"default_icon": "images/logo.png",
"default_title": "WP Version Detect | .is",
"default_popup": "popup.html"
},
"permissions": ["notifications", "storage", "tabs", "unlimitedStorage"]
}
为了避免重复,我现在不会详细讨论扩展所需的权限。如果需要更详细地讲解,我会在后面写出来。目前,重要的是理解我们只是列出了所需的 WebExtension API 对象。
background 属性
这些脚本在浏览器环境中运行。它们无法直接访问网站内容,但可以访问所有 WebExtensions API 接口:标签管理、上下文菜单项、书签、通知等。此外,后台脚本允许您轻松地向任何 URL 发出请求,而无需考虑任何策略。如有必要,您可以选择使用后台页面,将任何 URL 加载到其中。只需在后台添加“page”属性即可。此外,您可以轻松单独使用或与“脚本”一起使用。
后台脚本在浏览器环境中运行并不意味着它们是孤立的。扩展程序拥有一个消息传递系统,您可以通过它轻松传输数据。例如,我们将使用它在内容脚本运行结束时发出浏览器通知。此外,后台脚本可以轻松注入 JavaScript 代码。
在确定 WP 版本时,后台脚本起着辅助作用——它只是通知用户已确定版本:
JavaScript:复制到剪贴板
browser.runtime.onMessage.addListener(async (data, sender) => {
if (data.action == 'notify') {
browser.notifications.create({
type: "basic",
title: `Possible Wordpress Version: ${data.host}`,
message: `${data.host} site is probably built on Wordpress version ${data.maybeVersion.version}. This is indicated by ${data.maybeVersion.count} mentions of the version`,
});
}
return false
})
所有检测函数都分配给内容脚本,该脚本在执行时将保存数据并启动 sendMessage() 方法,将消息传递给浏览器。从上面的代码可以看出,后台脚本只是在 onMessage() 事件上挂起一个监听器,该监听器会拦截相同的 sendMessage() 方法。
接收消息时,可以接受三个参数:
- 数据。可以是任何类型,但我更喜欢对象。这样你可以传递事件类型和数据本身。
- 发件人 - 顾名思义,这是一个描述发件人的对象
- sendResponse - 一个用于返回消息响应的函数。我们的函数是异步的,通常不假设响应,因此我们始终返回“false”。
好吧,后台脚本的最后一个元素是启动“notifications.create”。事实上,这是一个非常糟糕的启动选项,因为通知有很多影响外观和行为的选项。如果需要,你甚至可以附加按钮。
content_scripts 属性
这些是嵌入到 Web 应用程序本身的脚本。浏览器连接内容脚本的方式几乎与网站所有者连接其部分 JS 的方式相同。脚本位于页面上下文中,可以与 DOM 交互、调用站点函数、更改变量值等。
“matches”属性用于设置内容脚本将加载到的 URL 模板。因此,您可以指定特定站点,例如,仅处理基于“http://*”的站点,或使用通用的“<all_urls>”。
“run_at”属性在您的扩展程序中起着关键作用,因为它指定了何时进行注入。在我们的例子中,脚本是在加载所有内容后添加的。
请注意,“js”和“scripts”(在后台)属性是数组。数组按照清单中指定的顺序加载。因此,如果需要通用变量或函数,则必须在第一个加载的脚本文件中声明它们。我们将在第二个扩展程序中使用此功能来添加新模块。
content.js WP 版本检测
确定 WordPress 版本:我们会从多个地方获取具有特定“概率权重”的潜在版本,然后将这些权重相加,并将权重最大的版本视为正确版本。一旦找到“正确版本”,我们会将值保存到本地存储并通知用户。让我们来看看具体步骤:
JavaScript:复制到剪贴板
(async function() {
...
})();
需要提醒的是,该脚本会在整个网站文档加载完成后加载。因此,我将代码封装在一个自调用函数中。我将其设置为异步,以便可以等待必要的异步函数执行,例如从本地存储获取数据:
JavaScript:复制到剪贴板
let savedValue = await browser.storage.local.get([window.location.host])
if (Object.keys(savedValue).length) return
目前,这些数据仅用于避免重复定义。否则,扩展程序会向您发送大量通知。JavaScript
:复制到剪贴板
let versions = []
const metaWP = document.querySelector('meta[name="generator"]')
if (metaWP) {
versions.push({
originalValue: metaWP.content,
version: metaWP.content.replace('WordPress ', ''),
type: 'Meta Generator',
weight: 10
})
}
如果我们有一个指示 WordPress 版本的元标记,我们会将这些信息保存在一个包含潜在版本的数组中。每个指示可能版本的选项都以对象的形式存储在版本数组中。我认为没有必要对每个属性都进行注释。JavaScript
:复制到剪贴板
const verRegexp = /(?<=wp-includes).*(?<=ver=)(.*)/
const getEntries = (query, param, title) => Array.from(document.querySelectorAll(query)).map(el => el[param].toString()).filter(el => verRegexp.exec(el)).map(el => ({
originalValue: el,
version: /(?<=ver=)\d\.\d{1,2}\.\d{1,2}/.exec(el)[0],
type: title,
weight: 1
}))
let cssWP = getEntries('link[rel="stylesheet"]', 'href', 'CSS Version')
let jsWP = getEntries('script', 'src', 'JS Version')
versions.push(...cssWP)
versions.push(...jsWP)
“ver”参数在确定版本方面起着重要作用,它通常与WP中的所有JS和CSS相关联。为了收集这些参数,我编写了getEntries()箭头函数。否则,我不得不几乎完全重复分别收集css和js的过程。尽管它们之间的区别仅在于搜索查询(“link”与“script”)和感兴趣的参数(“href”与“src”)。
该函数接收所有与搜索匹配的HTML对象,并将它们转换为常规数组中的HTML集合。但我们不需要对象,而是链接,因此第一个“映射”通过仅保留行来纠正不公平。使用过滤器,使用正则表达式“verRegexp”,我们删除了不包含“wp-include”和“ver”的行。然后,我们根据前一个数组的结构将行数组转换为对象数组。好了,我们通过解构将“versions”中获得的值组合并相加来完成操作。
下一步是向站点Feed发出请求。顺便说一句,此操作可以通过对许可证文件和自述文件的请求进行补充。JavaScript
:复制到剪贴板
try{
let response = await fetch(`${window.location.protocol }://${window.location.host }/feed/`)
let feedText = await response.text()
let feedRegex = /(?<=<generator>).*wordpress.*(\d\.\d{1,2}\.\d{1,2})(?=<\/generator>)/g
let feedGroups = feedRegex.exec(feedText)
if (feedGroups.length < 2) throw new Error('Empty wp')
versions.push({
originalValue: feedGroups[0],
version: feedGroups[1],
type: 'Feed Response',
weight: 5
})
} catch (e) {
console.log('Feed not found')
}
我将操作封装在 try-catch 中,因为可能没有 feed,而且我真的不想费心检查。虽然可以少写点代码……但
我认为以下步骤已经很清晰了:我用标准 fetch 执行了请求,等待结果,获取文本,用正则表达式提取值,并将另一个对象添加到潜在版本中。
现在是时候计算权重来确定胜者了:
JavaScript:复制到剪贴板
if (!versions.length)
return
let versionWeights = versions.reduce((acc, curr) => {
if (acc[curr.version]) {
acc[curr.version] += curr.weight
return acc
}
return {...acc, [curr.version]: curr.weight}
}, {})
let maybeVersion = {
version: "",
count: 0
}
for(let key in versionWeights) {
if (versionWeights[key] > maybeVersion.count)
maybeVersion = {
version: key,
count: versionWeights[key]
}
}
if (!maybeVersion.count) return browser.storage.local.set({[window.location.host]: {}})
剩下要添加的就是通知用户并保存结果。要通知用户,如后台脚本中所述,您需要使用 sendMessage() 方法将消息传递给运行时:
JavaScript:复制到剪贴板
browser.runtime.sendMessage({
action: 'notify',
host: window.location.host,
maybeVersion: maybeVersion
})
参数仅包含带有数据的对象,因为我们不期望任何响应。您可以安全地保存数据:
browser.storage.local.set({[window.location.host]: {versions, perhaps: perhapsVersion.version}})
我将更详细地讨论这一点。与任何网站一样,该扩展程序也拥有自己的存储。转到扩展程序调试 about:debugging#/runtime/this-firefox,然后点击“检查”或“探索”按钮。在打开的窗口中,选择“存储”:
屏幕截图显示数据保存在扩展程序存储中。在我们的例子中,这是本地存储。它在会话结束时不会被销毁,也不会与其他设备同步。数据存储在本地和一台设备上。有一些选项可以选择不使用“本地”,而是使用相同的“同步”,但在本例中没有意义。为了以防万一,以下是存储类型之间的区别:
- 本地 - 存储在计算机磁盘本地,不存储在其他位置。仅当扩展程序被移除或通过 API 强制移除时,数据才会被删除
- 同步 - 相同,但同步机制有效。请勿与存储中的同步录制混淆,录制始终是异步的。如果您在两台计算机上登录浏览器,存储数据将同步。对于工作狂来说非常方便。
- 会话 - 数据未保存到磁盘。RAM、缓存……关闭标签页后,数据已被删除。
- 托管 - 数据由域管理员设置,我们只能读取。从未使用过。在某些企业网络中,这种方法或许是可行的。
为了访问存储空间,我们在清单中申请了“存储”权限。但如果您仔细研究清单,就会发现权限中还有一项名为“unlimitedStorage”的权限。问题是,当仅申请存储空间访问权限时,存储空间被限制为 5MB。也就是说,扩展程序无法存储更多信息。通常情况下,5MB 还算可以,但我们比较慷慨,计划访问大量网站。因此,我指定了无限空间。只要用户配置文件在磁盘上可以容纳,空间就会被填满。只有删除扩展程序或清理存储空间时,才会删除数据。
存储空间中的数据是对象形式,因此在保存时,我们需要指定键和值。在本例中,主机名就是键。
内容脚本的完整代码:
JavaScript:复制到剪贴板
(async function() {
let savedValue = await browser.storage.local.get([window.location.host])
if (Object.keys(savedValue).length) return
let versions = []
const metaWP = document.querySelector('meta[name="generator"]')
if (metaWP) {
versions.push({
originalValue: metaWP.content,
version: metaWP.content.replace('WordPress ', ''),
type: 'Meta Generator',
weight: 10
})
}
const verRegexp = /(?<=wp-includes).*(?<=ver=)(.*)/
const getEntries = (query, param, title) => Array.from(document.querySelectorAll(query)).map(el => el[param].toString()).filter(el => verRegexp.exec(el)).map(el => ({
originalValue: el,
version: /(?<=ver=)\d\.\d{1,2}\.\d{1,2}/.exec(el)[0],
type: title,
weight: 1
}))
let cssWP = getEntries('link[rel="stylesheet"]', 'href', 'CSS Version')
let jsWP = getEntries('script', 'src', 'JS Version')
versions.push(...cssWP)
versions.push(...jsWP)
try{
let response = await fetch(`${window.location.protocol }://${window.location.host }/feed/`)
let feedText = await response.text()
let feedRegex = /(?<=<generator>).*wordpress.*(\d\.\d{1,2}\.\d{1,2})(?=<\/generator>)/g
let feedGroups = feedRegex.exec(feedText)
if (feedGroups.length < 2) throw new Error('Empty wp')
versions.push({
originalValue: feedGroups[0],
version: feedGroups[1],
type: 'Feed Response',
weight: 5
})
} catch (e) {
console.log('Feed not found')
}
if (!versions.length)
return
let versionWeights = versions.reduce((acc, curr) => {
if (acc[curr.version]) {
acc[curr.version] += curr.weight
return acc
}
return {...acc, [curr.version]: curr.weight}
}, {})
let maybeVersion = {
version: "",
count: 0
}
for(let key in versionWeights) {
if (versionWeights[key] > maybeVersion.count)
maybeVersion = {
version: key,
count: versionWeights[key]
}
}
if (!maybeVersion.count) return browser.storage.local.set({[window.location.host]: {}})
browser.runtime.sendMessage({
action: 'notify',
host: window.location.host,
maybeVersion: maybeVersion
})
browser.storage.local.set({[window.location.host]: {versions, maybe: maybeVersion.version}})
})();
action 属性
JSON:复制到剪贴板
"action": {
"default_icon": "images/logo.png",
"default_title": "WP Version Detect | .is",
"default_popup": "popup.html"
},
这是定义应用栏图标的地方。我们只需告诉浏览器要使用什么图标、要显示什么工具提示,并指定一个将以弹出窗口形式显示的常规 HTML 文档即可。顺便说一下,“WP 版本检测”窗口的 HTML 代码如下:
HTML:复制到剪贴板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WP Version Detect | .is</title>
</head>
<body>
<script src="popup.js"></script>
<div id="header" style="padding: 10px 0;">
<span id="title">Detected WP version:</span>
<span id="version"></span>
</div>
<table>
<thead>
<tr>
<th>Type</th>
<th>Version</th>
<th>Full value</th>
</tr>
</thead>
<tbody id="list">
</tbody>
</table>
</body>
</html>
我没有费心设计界面,关键在于演示原理。界面的本质很简单:顶部是脚本认为最有可能的版本,底部是一个表格,其中包含脚本计算的参数。popup.js
脚本负责窗口的填充和行为:
JavaScript:复制到剪贴板
(async function() {
let activeTab = await browser.tabs.query({active:true, currentWindow: true})
let url = activeTab[0].url
let domain = (new URL(url)).hostname
browser.storage.local.get([domain], savedValue => {
let data = savedValue[domain]
let list = document.querySelector('#list')
if (!data) return
document.querySelector('#version').innerHTML = data.maybe
list.innerHTML = ''
data.versions.forEach(el => {
const tr = document.createElement('tr')
const tdType = document.createElement('td')
const tdVer = document.createElement('td')
const tdVal = document.createElement('td')
tdType.innerHTML = el.type
tdVer.innerHTML = el.version
tdVal.innerHTML = el.originalValue
tr.append(tdType, tdVer, tdVal)
list.append(tr)
})
})
})()
我把所有代码都封装在一个异步的自启动函数里。之所以采用异步的方式,是为了避免构建一堆回调函数。我会弄清楚这里发生了什么:
JavaScript:复制到剪贴板
let activeTab = await browser.tabs.query({active:true, currentWindow: true})
let url = activeTab[0].url
let domain = (new URL(url)).hostname
要从本地存储中检索数据,我们需要主机名。您还记得,主机名是扩展存储的键。弹出脚本无法直接访问网站内容和上下文,因此无法从 location 变量获取主机名。我们必须通过请求活动窗口的活动选项卡来访问选项卡。理论上,我们可以忽略“currentWindow”,但如果打开了多个窗口,这可能会有问题。
要使用“tabs”,我们需要在 manifest.json 中指定此权限。这次,我们只是表面上使用“tabs”对象,但实际上,它是一个强大的工具,它不仅允许我们像普通用户一样以编程方式管理选项卡,还可以响应事件。
一旦我们收到域名,我们就可以从本地存储中加载数据并显示它。为此,我们使用本地存储的 get() 方法,将键和处理结果的函数传递给它:
JavaScript:复制到剪贴板
browser.storage.local.get(domain, savedValue => {
let data = savedValue[domain]
let list = document.querySelector('#list')
if (!data) return
document.querySelector('#version').innerHTML = data.maybe
list.innerHTML = ''
data.versions.forEach(el => {
const tr = document.createElement('tr')
const tdType = document.createElement('td')
const tdVer = document.createElement('td')
const tdVal = document.createElement('td')
tdType.innerHTML = el.type
tdVer.innerHTML = el.version
tdVal.innerHTML = el.originalValue
tr.append(tdType, tdVer, tdVal)
list.append(tr)
})
})
我认为解释数据输出到界面的过程毫无意义。这通常用于处理 HTML 文档。
现在,我们将先把用于检测 Wordpress 版本的扩展放在一边。当然,我甚至不想称这种确定版本的算法准确,但它仍然能够给出良好的结果。不过,最重要的是,您已经从头编写了一个相当有用的扩展,同时还处理了页面主体、消息系统和扩展存储。现在是时候讨论更有趣的内容了。
第二次扩张
正如我之前所写,此扩展主要由可添加和扩展的独立模块组成。模块化将基于向数组添加新脚本(包括背景脚本和内容脚本)的能力来实现。JSON
:复制到剪贴板
{
"manifest_version": 3,
"name": "Passive Hack | .is",
"description": "Extension for passive scanning of web applications. Developed specifically for the article on the .is website",
"version": "0.0.1",
"background": {
"scripts": [... ,"js/background.js"]
},
"action": {
"default_icon": "images/logo.png",
"default_title": "Passive Hack | .is",
"default_popup": "popup/popup.html"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["js/content.js", ...],
"run_at": "document_end"
}
],
"permissions": [
"tabs",
"cookies",
"webRequest",
"storage",
"unlimitedStorage"
]
}
因此,将使用脚本代替省略号。最终的 manifest.json 文件将在文章末尾提供。请注意,background 中的省略号位于 background.js 脚本之前,内容脚本中则相反。主后台脚本将包含一个事件处理程序,用于调用其他脚本中的函数。这意味着所有被调用的函数都必须在主脚本之前声明。在内容脚本中,可复用函数将放在通用文件中,因此它位于最前面。
从清单中可以看出,文件夹结构如下:
- js - 所有与内容和背景相关的脚本都将存储在这里
- popup - 与弹出窗口相关的所有内容的文件夹
- 图像 - 项目图像
您可以自行定义文件夹结构,没有严格的要求。
除了上面列出的文件夹外,我还添加了一个 bootstrap 文件夹,其中包含 css 和 js,以方便美观。
贮存
由于扩展将是一个相当脱节的结构,并且需要组织保存许多单独对象的过程,因此我决定编写一个单独的函数来保存数据:
JavaScript:复制到剪贴板
function saveData(hostname, key, object) {
browser.storage.local.get([hostname], result => {
browser.storage.local.set({[hostname]: {...result[hostname], [key]: object}})
})
}
该函数接受主机名、存储键和要存储的对象。首先,它通过主机获取当前保存的对象,然后通过析构保存该对象,覆盖特定的键。
为了以防万一,需要为不太熟悉 JavaScript 特性的人做一些解释。当写入 [key] 和 [domain] 时,我们获取存储在变量中的键的名称。如果只写“key”,那么键的名称就是“key”。当使用方括号时,键的名称就是存储在“key”中的内容。
覆盖数据可以想象如下:
让我们回到我们的代码。现在,保存数据可以通过两种方式完成,具体取决于我们想要从哪里保存:
JavaScript:复制到剪贴板
browser.runtime.sendMessage({
action: 'saveData',
hostname: window.location.hostname,
key: 'comments',
object: [...comments]
})
因此,如果我们想从内容脚本或弹出脚本传递数据进行保存,那么在后台脚本中,我们直接调用保存函数:
JavaScript:复制到剪贴板。saveDataFunction(hostname, 'cookies', {...cookies})
请注意,在后台脚本中,我调用的是“saveDataFunction”,而不是“saveData”。这完全取决于作用域。由于后台脚本函数位于不同的文件中,它们可能彼此不了解。因此,在调用收集数据的函数时,我通过附加参数“saveDataFunction”将保存函数作为回调传递,然后将其作为函数调用:
JavaScript:复制到剪贴板
// Функция обработки кук в файле bg_cookie.js
function checkCookies(hostname, cookiesData, saveDataFunction) {
...
// Обработка данных
...
// Вызываю callback
saveDataFunction(hostname, 'cookies', {...cookies})
}
// Основная функция в background.js, которые вызывает checkCookies()
browser.runtime.onMessage.addListener(async (data, sender, sendResponse) => {
...
} else if (data.action == 'getCookies') {
cookies = await browser.cookies.getAll({
hostname: data.hostname
})
// Передаю saveData как callback
checkCookies(data.hostname, cookies, saveData)
}
...
})
寻找敏感内容
让我们从简单的事情开始。页面源代码可以提供很多有用的信息,您可能会偶然发现开发人员忘记删除的敏感评论、收集链接、确定技术等等。我将重点介绍评论。是的,在评论中找到凭据的概率几乎为零,但有时您会遇到一些有趣的东西……评论指向管理面板的链接、子域名或 IP 地址的提及。在开发过程中,程序员会留下各种各样的注释。我总是会检查评论,现在是时候让这个过程变得方便了。
我们将使用字符串的“search”方法进行搜索。其妙处在于,我们可以在其中输入字符串或正则表达式。cs_comments.js 文件的代码:
JavaScript:复制到剪贴板
const html = document.body.innerHTML
commentsRegex = /<!--[^>]*-->/g
sensetiveSearchValues = ["passwd", "password", "pass", "creds", /(?<=db).*/, /(https|http|ftp)(.*)/ ,/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/]
const comments = html.match(commentsRegex).map( comment => {
const sensValues = sensetiveSearchValues.map(sens => {
if (comment.search(sens) != -1)
return sens
return false
}).filter(Boolean)
return {
comment,
sensValues
}
})
browser.runtime.sendMessage({
action: 'saveData',
hostname: window.location.hostname,
key: 'comments',
object: [...comments]
})
请注意,“sensetiveSearchValues”数组仅用作示例。如果您不知道用什么填充此数组,可以查看以下字典:但即使您不填充字典,处理评论也比深入研究源代码方便得多。
代码逻辑非常简单,不太可能引起很多问题。您获取页面的 HTML 代码,声明用于搜索评论的正则表达式以及一个包含感兴趣匹配项的数组。然后,通过将正则表达式应用于页面代码,您将获得一个需要检查“兴趣度”的匹配项数组。“map”函数有助于此,其中的输出被替换为一个对象。该对象仅包含两个属性:
- 评论行本身
- 有趣的事件——这里是产生匹配的精确字符串或正则表达式。
脚本以保存数据结束:
JavaScript:复制到剪贴板
browser.runtime.onMessage.addListener(onMessageHandler)
async function onMessageHandler(data) {
if (data.action == 'saveData' && data.key && data.object) {
if (typeof data.object == 'string')
data.object = JSON.parse(data.object)
return saveData(data.hostname, data.key, data.object)
}
return false
}
请注意,在函数末尾我返回了“false”。这是必要的,以防任何 if 语句未返回。务必从异步 onMessage 事件处理程序返回,否则会由于误解函数执行完成的脚本而出现错误。
第一个结果存储在存储中:
我保存了条目,以便在输出时获得最完整的信息。顺便说一下,弹出窗口文件目前如下所示:
HTML:复制到剪贴板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../bootstrap/bootstrap.min.css">
<script src="../bootstrap/bootstrap.min.js"></script>
<title>Document</title>
</head>
<body style="width: 600px;padding: 20px 0; ">
<div class="container">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Comments</a>
</li>
</ul>
<div class="accordion" id="comments">
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
没什么特别的,一个连接了 Bootstrap 的简单项目。除了设置了 body 宽度之外,浏览器在创建弹出窗口时会考虑这个宽度。
所有“魔法”都发生在 popup.js 文件中:
JavaScript:复制到剪贴板
(async function() {
let activeTab = await browser.tabs.query({active:true, currentWindow: true})
let url = activeTab[0].url
let domain = (new URL(url)).hostname
browser.storage.local.get(domain, savedValue => {
let data = savedValue[domain]
if (!data || !data.comments || !data.comments.length) return
let commentsDiv = document.querySelector('#comments')
const commentsHTML = data.comments.map((comment, ind) => {
console.log(comment.sensValues.length, comment.comment)
let body, button = `<span class="accordion-button collapsed text-success">${comment.sensValues.length} | ${comment.comment.replaceAll('<', '<')}</span>`
if (comment.sensValues.length) {
button = `<button class="accordion-button collapsed text-danger" type="button"
data-bs-toggle="collapse" data-bs-target="#collapse${ind}"
aria-expanded="false" aria-controls="collapse${ind}">
${comment.sensValues.length} | ${comment.comment.replaceAll('<', '<')}
</button>`
body = `<div class="accordion-body">${comment.sensValues.join(', ')}</div>`
}
return `<div class="accordion-item">
<h2 class="accordion-header">
${button}
</h2>
<div id="collapse${ind}" class="accordion-collapse collapse" data-bs-parent="#accordionExample">
${body}
</div>
</div>`
})
commentsDiv.innerHTML = commentsHTML.join('')
})
})()
如果您阅读了确定WP版本的部分,这段代码对您来说会非常熟悉。我们再次获取标签页,再次获取主机名,等等。不同的是,输出是通过为带有折叠面板的div设置innerHTML来实现的。如果匹配成功,则会显示匹配的数量,并以红色高亮显示文本,否则会显示“0”和绿色。
输出之前,“<”会被html代码替换。这当然很有趣……但我花了大约10分钟都没弄清楚代码到底出了什么问题……为什么评论文本没有显示……))))
这样就完成了评论的处理。但请记住,每次加载页面时,活动域中的评论数据都会被重写。我想您知道如何解决这个问题。
了解 Cookie
有趣的是,我最近遇到了一个带有“角色”cookie 的资源。任何用户都可以将“用户”设置为“管理员”,并顺利进入管理面板。类似的 bug 可能会在某些老版本的 Web 应用程序中持续存在数年。Cookie 通常以 base64、JSON 或其他格式编码。最好实现一个检查器,在后台悄悄地规范化 Cookie,并检查其中的一些有趣参数。例如:角色、ID、用户 ID 等。
因此,扩展程序可以从两个方面访问 Cookie:从 Web 应用程序上下文和从浏览器内容。我们将从浏览器上下文进行操作,因为在这种情况下,没有任何限制。最重要的是不要忘记在清单中所需的权限列表中写入“cookies”。JavaScript
:复制到剪贴板
let getting = browser.cookies.getAll(
details // object
)
要获取 Cookie,只需调用 getALL() 方法并传入过滤器即可。如果不传入过滤器,浏览器将转储所有 Cookie。我们可以每次处理所有可用的 Cookie,但最好至少将它们限制在一个域内。这样,调用方式将类似于:
JavaScript:复制到剪贴板
cookies = await browser.cookies.getAll({
domain:domain
})
说到过滤器,种类繁多,您可以根据自己的配置使用它们……可以组合使用,也可以单独使用。例如,我们可以请求浏览器中存储的所有 httpOnly Cookie:
JavaScript:复制到剪贴板
cookies = await browser.cookies.getAll({
secure: true
})
我们的扩展将与活动页面一起工作,因此我们在内容脚本“cs_cookie.js”中编写代码(不要忘记将脚本的路径添加到清单中):
JavaScript:复制到剪贴板
let response = browser.runtime.sendMessage({
action: 'getCookies',
domain: window.location.hostname
})
在 background.js 中我们拦截消息并开始处理:
JavaScript:复制到剪贴板
async function onMessageHandler(data) {
if (data.action == 'saveData' && data.key && data.object) {
if (typeof data.object == 'string')
data.object = JSON.parse(data.object)
return saveData(data.hostname, data.key, data.object)
} else if (data.action == 'getCookies') {
let cookies = await browser.cookies.getAll({
domain: data.hostname
})
checkCookies(data.hostname, cookies, saveData)
return false
}
return false
})
如果我们直接输出 .is 格式的 cookies,我们会看到类似这样的内容:
让我们开始编写 checkCookies() 检查器吧。其工作流程如下:
- 我们检查 Cookie 值是否是 JWT 令牌。如果是,则解密所有参数并继续分别分析。
- 我们尝试从 base64 解码,并添加缺失的字符以防万一。
- 分析值是JSON还是XML
- 我们寻找一行中敏感数据的线索
在代码中我这样表达(bg_cookie.js):
JavaScript:复制到剪贴板
function checkCookies(hostname, cookiesData, saveDataFunction) {
let cookies = cookiesData.map(({name, value, secure, session}) => {
let cookieDecode = parseJwt(value)
let cookieBS64Decode = checkBase64Value(cookieDecode)
let isJSON = checkJSONValue(cookieBS64Decode)
let isXML = checkXMLValue(cookieBS64Decode)
let sensName = checkSensCookies(name)
let sensValue = checkSensCookies(cookieBS64Decode)
return {
name, value, secure, session, isJSON, isXML, sensName, sensValue, base64decode: cookieDecode == cookieBS64Decode ? null : cookieBS64Decode
}
})
saveDataFunction(hostname, 'cookies', cookies)
}
function checkSensCookies(value) { ... }
function checkBase64Value(originalValue) { ... }
function checkXMLValue(value) { ... }
function checkJSONValue(value) { ... }
function parseJwt (token) { ... }
只需使用专门的函数按顺序遍历每个 Cookie。仔细观察变量的位置和使用情况,以确定该函数或该函数应用于哪些数据。最好添加一些控制台日志并查看输出。JavaScript
:复制到剪贴板
function parseJwt (token) {
let parts = token.split('.')
if (parts != 3) return token
return parts[1]
}
一切都很简单,JWT 由三个独立的部分组成。如果不是,则返回收到的值。如果是,则返回平均值,因为它是 base64 编码的,下一步我们将尝试解码该值:
JavaScript:复制到剪贴板
function checkBase64Value(originalValue) {
let checkedValue = originalValue
while(checkedValue.length % 4) checkedValue += '='
try {
decodeValue = atob(checkedValue)
return decodeValue
} catch (except) {
return originalValue
}
}
因此,我们首先将缺失的符号增加到正确的值,然后进行解码。与上一个函数一样,如果出现错误,我们将返回输入值。JavaScript
:复制到剪贴板
function checkXMLValue(value) {
try{
const parser = new DOMParser()
const doc = parser.parseFromString(value, "application/xml")
const errorNode = doc.querySelector("parsererror")
if (errorNode) {
return false
} else {
return true
}
} catch (e) {
return false
}
}
function checkJSONValue(value) {
try {
const json = JSON.parse(value)
return true
} catch(e) {
return false
}
}
这些函数很简单,而且完全符合它们的名称,所以我就不赘述了。文件末尾有一个用于检查有趣值的函数:
JavaScript:复制到剪贴板
function checkSensCookies(value) {
const sensetiveCookieNames = ['role', 'token', 'secret', 'id', 'userid', 'username', 'password']
const sensValues = sensetiveCookieNames.map(sens => {
if (value.search(sens) != -1)
return sens
return false
}).filter(Boolean)
return {value, sensValues}
}
与评论一样,我们只需搜索出现次数即可。现在开始输出,输出结果与评论信息的输出非常相似。我将从 popup.html 开始,并立即在其中输入我最终想要看到的内容。更准确地说,我将添加用于在不同信息块和所有信息块之间切换的选项卡:
HTML:复制到剪贴板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="../bootstrap/bootstrap.min.css">
<script src="../bootstrap/bootstrap.min.js"></script>
<title>Document</title>
</head>
<body style="width: 600px;padding: 20px 0; ">
<div class="container">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" data-id="comments"href="#">Comments</a>
</li>
<li class="nav-item">
<a class="nav-link" data-id="cookies"href="#">Cookies</a>
</li>
<li class="nav-item">
<a class="nav-link" data-id="links"href="#">Links</a>
</li>
<li class="nav-item">
<a class="nav-link" data-id="dns"href="#">DNS Info</a>
</li>
<li class="nav-item">
<a class="nav-link" data-id="subdomains"href="#">Subdomains</a>
</li>
<li class="nav-item">
<a class="nav-link active" data-id="about"href="#">About</a>
</li>
</ul>
<div class="accordion tab-div" id="comments" style="display: none;">
Comments is empty
</div>
<div class="accordion tab-div" id="cookies" style="display: none;">
Cookies is empty
</div>
<div class="accordion tab-div" id="links" style="display: none;">
<h2>Parsed from page</h2>
<div class="container">
</div>
<h2>Links is empty</h2>
<button id="responseMonitoring">Start background monitoring</button>
<ul id="parsedLinks">
</ul>
</div>
<div class="container tab-div" id="dns" style="display: none;">
<h3>Canonical Name</h3>
<span id="dnsCanonicalName"></span>
<h3>IP</h3>
<ul id="dnsIP">
</ul>
</div>
<div class="container tab-div" id="subdomains" style="display: none;">
<h2>Subdomains</h2>
<button id="loadSubdomains">Parse from CRT.sh</button>
<ul id="subdomains-list-crt"></ul>
<h2>Other platforms:</h2>
<button id="loadDNSInfo">Open tabs</button>
</div>
<div class="container tab-div" id="about">
<h2>About</h2>
<p>
Расширение не является готовым рабочим решением. Данное расширение было разработано исключительно в образовательных целей для статьи на сайте <a href="</a>. Прочитав статью, вы можете спокойно модифицировать расширение под свои задачи. Код поставляется "как есть" и не поддерживается автором.
</p>
<p>
This extension was developed for educational purposes only for the <a href="</a> website. After reading the article, you can modify the extension to suit your needs. The code is provided "as is" and is not maintained by the author.
</p>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
因此,您需要在 popup.js 中挂起处理标签点击的处理程序。JavaScript
:复制到剪贴板
document.querySelectorAll('.nav-link').forEach(navLink => {
navLink.addEventListener('click', event => {
event.preventDefault()
document.querySelectorAll('.nav-link').forEach(elem => elem.classList.remove('active'))
event.currentTarget.classList.add('active')
document.querySelectorAll('.tab-div').forEach(elem => elem.style.display = 'none')
document.querySelector(`#${event.currentTarget.dataset.id}`).style.display = 'block'
})
})
至于 Cookie 信息的输出,它将与评论完全相同。展望未来,我会说,页面解析链接的输出将完全相同。构建三个相同的函数,仅在几个参数上有所不同……嗯,不行。最好创建一个单独的服务函数。我不喜欢的另一点是,我们直接在弹出脚本中获取 URL。好的方案是,即使与存储的交互也应该被拉到后台脚本中。但至少我把 URL 拖到了后台,尤其是因为它也需要通过
JavaScript 获取:复制到剪贴板
let url
(async function() {
url = await getCurrentURL()
browser.storage.local.get([url.hostname], savedValue => {
let data = savedValue[url.hostname]
if (!data) return
if (data.comments && data.comments.length) printComments(data)
if (data.cookies && data.cookies.length) printCookies(data)
})()
async function getCurrentURL() {
return new URL(await browser.runtime.sendMessage({action: 'getCurrentTabUrl'}))
}
function printComments({comments}) {
printCheckedValues('#comments', comments, 'comment')
}
function printCookies({cookies}) {
printCheckedValues('#cookies', cookies, 'cookie')
}
function printCheckedValues(selector, data, paramName) {
let elemDiv = document.querySelector(selector)
const elemsHTML = data.map((item, ind) => {
console.log(item.sensValues.length, item[paramName])
let body, button = `<span class="accordion-button collapsed text-success">${item.sensValues.length} | ${item[paramName].replaceAll('<', '<')}</span>`
if (item.sensValues.length) {
button = `<button class="accordion-button collapsed text-danger" type="button"
data-bs-toggle="collapse" data-bs-target="#collapse${ind}"
aria-expanded="false" aria-controls="collapse${ind}">
${item.sensValues.length} | ${item[paramName].replaceAll('<', '<')}
</button>`
body = `<div class="accordion-body">${item.sensValues.join(', ')}</div>`
}
return `<div class="accordion-item">
<h2 class="accordion-header">
${button}
</h2>
<div id="collapse${ind}" class="accordion-collapse collapse" data-bs-parent="#accordionExample">
${body}
</div>
</div>`
})
elemDiv.innerHTML = elemsHTML.join('')
}
现在好多了。如果您需要另一个类似的输出,只需添加一个带参数的新打印函数即可。但别忘了 background.js 也需要修改:
JavaScript:复制到剪贴板
let currentURL, currentUrlObj
async function getCurrentURL(activeInfo) {
let activeTab = await browser.tabs.query({active:true, currentWindow: true})
currentURL = activeTab[0].url
currentUrlObj = new URL(currentURL)
return currentURL
}
browser.runtime.onInstalled.addListener(() => {
browser.tabs.onActivated.addListener(getCurrentURL)
browser.runtime.onMessage.addListener(onMessageHandler)
})
function saveData(hostname, key, object) {
browser.storage.local.get([hostname], result => {
browser.storage.local.set({[hostname]: {...result[hostname], [key]: object}})
})
}
async function onMessageHandler(data) {
if (data.action == 'saveData' && data.key && data.object) {
if (typeof data.object == 'string')
data.object = JSON.parse(data.object)
return saveData(data.hostname, data.key, data.object)
} else if (data.action == 'getCurrentTabUrl') {
if (currentURL) return currentURL
return currentURL
} else if (data.action == 'getCookies') {
let cookies = await browser.cookies.getAll({
domain: data.hostname
})
checkCookies(data.hostname, cookies, saveData)
return false
}
return false
}
首先,添加了两个变量来存储 URL:一个是文本,另一个是 URL 对象。这两个变量都由 getCurrentURL() 函数接收。此函数会在两种情况下调用:用户切换标签页时,或者通过“getCurrentTabUrl”消息发出请求时变量为空时。
解析页面中的链接
没有必要详细讨论该过程,因为它几乎与评论和 cookie 的处理相同,所以让我们直接进入 cs_linkSearch.js
JavaScript 代码:复制到剪贴板
sensetiveLinkValues = ["admin", "control", "api", "panel", "dev"]
const aHref = Array.from(document.querySelectorAll('a')).map(link => encodeURI(link.href).toString()).filter(Boolean)
const linkHref = Array.from(document.querySelectorAll('link')).map(link => encodeURI(link.href).toString()).filter(Boolean)
const scriptSrc = Array.from(document.querySelectorAll('script')).map(link => encodeURI(link.src).toString()).filter(Boolean)
const allLinks = Array.from(new Set([...aHref, ...linkHref, ...scriptSrc]))
const pageLinks = allLinks.map( link => {
if (link[0] == '#') return false
const sensValues = sensetiveLinkValues.map(sens => {
if (link.search(sens) != -1)
return sens
return false
}).filter(Boolean)
return {
link,
sensValues
}
}).filter(Boolean)
browser.runtime.sendMessage({
action: 'saveData',
hostname: window.location.hostname,
key: 'pageLinks',
object: JSON.stringify(pageLinks)
})
我简单地收集了所有标签 a、link、script。我清除了重复项,并查找敏感条目,例如“admin”等等。请注意,我传递了一个链接来将对象保存到字符串中。链接数量可能非常多,所以我谨慎行事。
只需稍微修改一下输出 (popup.js):
JavaScript:复制到剪贴板
...
if (data.pageLinks && data.pageLinks.length) printPageLinks(data)
...
function printPageLinks({pageLinks}) {
printCheckedValues('#links', pageLinks, 'link')
}
请求/响应处理
为了更深入地了解,我们需要 webRequest 对象。该对象允许您在大量阶段跟踪和处理请求和响应。在 Mozilla 开发者门户上,有一个图表演示了请求/响应的所有阶段。该图表几乎适用于所有黑客扩展程序,除非我们讨论一些无关紧要的引用:
分析请求的另一种方法是使用 API 对象“proxy”。在本文中,我不会对其进行分析,如果您愿意,可以将其添加到权限清单中,然后通过 addListener 方法在后台脚本中订阅 proxy.onRequest 事件,并自行研究可以获取哪些信息。JavaScript
:复制到剪贴板
browser.proxy.onRequest.addListener(
listener, // function
filter, // object
extraInfoSpec // optional array of strings
)
在本文中,我们将分析接收到的数据并搜索其中的链接。当然,这并非唯一的用例。您可以收集发送的参数、搜索 API 端点、分析请求以查找 SSRF 漏洞等等。甚至可以在全自动模式下修改请求并搜索 或其他漏洞。我们稍后会讨论这个问题,但现在我们先订阅以下事件:
JavaScript:复制到剪贴板
function beforeRequestHandler(requestInfo) {
console.log('requestInfo', requestInfo)
}
browser.webRequest.onBeforeRequest.addListener(beforeRequestHandler,
{
urls: ["<all_urls>"],
types: ['main_frame', 'script', 'xml_dtd', 'xmlhttprequest'],
}, ['blocking', 'requestBody']
);
顾名思义,该事件发生在发送请求之前。分配监听器时,我会向其传递一个监听函数、一个带有过滤器的对象以及一个包含额外参数的数组。过滤器允许您忽略不必要的内容,例如图像。或者限制处理请求的地址。额外参数有助于自定义行为。
“details”参数会传递给监听器函数,因为它在 MND 中被调用(在我的例子中是“requestInfo”),它包含一个完整描述请求的对象。
至于过滤器对象……我使用了两个参数:urls 和 types,它们都是数组。通常,它们会自行记录。所有 URL 都将被处理,并从中选择以下内容:
- Main_frame 是服务器返回的主文档(例如 html 页面)
- 脚本
- XML 文件也可以包含链接,我们正在专门寻找链接。
该示例传递了 requestBody 和 blocking 参数。“requestBody”告诉 FF,它需要将请求主体添加到包含请求详细信息的对象中。“blocking”是一个更有趣的参数。没有它,我们将无法获得服务器响应。此外,使用此参数,我们可以在发送请求之前对其进行更改或完全取消请求。例如,如果您的扩展程序在发送之前分析了请求并发现了一些危险数据,则有必要这样做。然后,您可以自定义行为,例如,将此类请求转发到阻止页面或直接取消请求。
从最近的示例中,我记得一个目标,其中向用户和管理员发送了有关用户个人资料任何更改的通知。我进入管理面板并想查找辅助 SQLi,但每次保存时,我都恼火地发现 sendNotify.php 上都有一个额外的问题。是的,这很奇怪,因为发送通知可以附加到发送函数中,但我又有什么资格与该资源的开发者争论呢?
为了最大限度地降低被扩展程序检测到的风险,只需为域名添加一个额外的阻止参数 ["blocking"] 即可。此外,对于包含“sendNotify.php”的请求,将返回值替换为 {cancel: true}; 。别忘了在清单中添加“webRequestBlocking”权限。JavaScript
:复制到剪贴板
function beforeRequestHandler(requestInfo) {
if (requestInfo.url.indexOf('sendNotify.php') > -1)
return {cancel: true}
}
browser.webRequest.onBeforeRequest.addListener(beforeRequestHandler, {
urls: ["https://your_target.com/*"],
["blocking"]
});
另一个选项是返回“redirectUrl”,而不是“cancel”,并在其中传递所需的 URL。例如,在尝试获取成人内容时,忽略“atat-page”。
让我们回到我们的扩展程序。我将在清单中添加权限:
JavaScript:复制到剪贴板
"webRequest",
"webRequestBlocking",
"webRequestFilterResponse",
我将重写 beforeRequestHandler() 以添加响应过滤:
JavaScript:复制到剪贴板
function beforeRequestHandler(requestInfo) {
let filter = browser.webRequest.filterResponseData(requestInfo.requestId)
let decoder = new TextDecoder("utf-8")
filter.ondata = (event) => {
let str = decoder.decode(event.data, {stream: true});
searchLinks(str)
filter.write(event.data)
};
filter.onstop = (event) => {
filter.disconnect()
}
}
browser.webRequest.onBeforeRequest.addListener(beforeRequestHandler,
{
urls: ["<all_urls>"],
types: ['main_frame', 'script', 'xml_dtd', 'xmlhttprequest'],
}, ['blocking', 'requestBody']
);
请注意,我们接收的不是就绪数据,过滤器返回的是数据流,我们需要处理“ondata”事件。此外,您不能随意处理该流:事件处理会阻塞流,因此在处理结束时必须强制写入 event.data 中接收到的数据流。否则,请求将被中断,数据将无法输出。顺便说一句,解码流后,您可以像处理常规字符串变量一样轻松地对其进行更改,从而改变应用程序的行为……但我们今天的任务是查找链接。为此,我将创建一个辅助函数,该函数将使用正则表达式遍历数据并保存接收到的值。我们将使该函数异步化,以尽可能避免在处理过程中阻塞流。
您需要一些正则表达式。您可以费心编写自己的版本,但是既然已经有现成的版本了,为什么还需要它呢?我建议您使用 JS Link Finder 存储库,这是一个相当流行的 Burp 扩展。他们有一个相当有趣的正则表达式。Python
:复制到剪贴板
regex_str = """
(?:"|') # Start newline delimiter
(
((?:[a-zA-Z]{1,10}://|//) # Match a scheme [a-Z]*1-10 or //
[^"'/]{1,}\. # Match a domainname (any character + dot)
[a-zA-Z]{2,}[^"']{0,}) # The domainextension and/or path
|
((?:/|\.\./|\./) # Start with /,../,./
[^"'><,;| *()(%%$^/\\\[\]] # Next character can't be...
[^"'><,;|()]{1,}) # Rest of the characters can't be
|
([a-zA-Z0-9_\-/]{1,}/ # Relative endpoint with /
[a-zA-Z0-9_\-/]{1,} # Resource name
\.(?:[a-zA-Z]{1,4}|action) # Rest + extension (length 1-4 or action)
(?:[\?|/][^"|']{0,}|)) # ? mark with parameters
|
([a-zA-Z0-9_\-]{1,} # filename
\.(?:php|asp|aspx|jsp|json|
action|html|js|txt|xml) # . + extension
(?:\?[^"|']{0,}|)) # ? mark with parameters
)
(?:"|') # End newline delimiter
"""
你只需要删除注释和换行符来整理一下。输出如下函数:
JavaScript: Copy to clipboard
async function searchLinks(text) {
let linkRegEx = /(?:"|')(((?:[a-zA-Z]{1,10}:\/\/|\/\/)[^"'/]{1,}\.[a-zA-Z]{2,}[^"']{0,})|((?:\/|\.\.\/|\.\/)[^"'><,;| *()(%%$^/\\\[\]][^"'><,;|()]{1,})|([a-zA-Z0-9_\-/]{1,}\/[a-zA-Z0-9_\-/]{1,}\.(?:[a-zA-Z]{1,4}|action)(?:[\?|/][^"|']{0,}|))|([a-zA-Z0-9_\-]{1,}\.(?:php|asp|aspx|jsp|json|action|html|js|txt|xml)(?:\?[^"|']{0,}|)))(?:"|')/ig
console.log(typeof text, text)
text.matchAll(linkRegEx, "ig").forEach(console.log)
}
测试成功,剩下的就是删除重复项和未定义项,并将所有内容写入链接。
但有一个细微差别……如果监控所有响应,系统将严重负载。除了整个浏览器中的每个响应都需要通过正则表达式运行之外,清除重复项和不断保存的过程也会给系统带来负担。我认为最佳方案是启用用户监控。弹出窗口中已经添加了一个按钮,剩下的就是为其添加动画效果并更改启动监控的过程。我将从 popup.js 开始:
JavaScript:复制到剪贴板
(async function() {
...
initMonitoringButton(await checkRunningMonitoring())
checkHasParsedLinks()
...
})()
...
async function checkHasParsedLinks() {
let response = await browser.runtime.sendMessage({
action: 'hasParsedLinks'
})
if (response.length) appendParsedLinks(response)
}
async function checkRunningMonitoring() {
let response = await browser.runtime.sendMessage({
action: 'checkMonitoring'
})
return response == 'running'
}
async function initMonitoringButton(running = false) {
let btn = document.querySelector('#responseMonitoring')
async function start(event) {
event.preventDefault()
console.log('Start Monitoring')
await browser.runtime.sendMessage({
action: 'startMonitoring'
})
initMonitoringButton(await checkRunningMonitoring())
}
async function stop(event) {
event.preventDefault()
console.log('Stop Monitoring')
await browser.runtime.sendMessage({
action: 'removeMonitoring'
})
initMonitoringButton(await checkRunningMonitoring())
}
if (running) {
btn.innerHTML = `Stop`
btn.removeEventListener('click', start)
btn.addEventListener('click', stop)
} else {
btn.innerHTML = `Start`
btn.removeEventListener('click', stop)
btn.addEventListener('click', start)
}
}
browser.runtime.onMessage.addListener(updateParsedInfoHandler)
async function updateParsedInfoHandler(data) {
if (data.action == 'response_links_update') {
appendParsedLinks(data.newLinks)
}
}
出现了两个函数:一个负责按钮行为,另一个检查是否已解析链接,如果是,则添加。关键在于,为了正确解析链接,您需要拦截请求,包括页面初始加载期间的请求,以及看似没有任何反应但网站已加载完成的请求。第二个细微差别是,每次打开弹窗时都会触发 popup.js 脚本。每次打开弹窗都是一个空白页。如果不设置某种临时存储,每次打开时链接都会从界面中删除。
为了解决这个问题,我设置了已解析链接的临时存储。按下“开始”按钮时,请求拦截器将启动,链接列表将被清除,页面将更新。拦截器将找到的链接写入存储,直到按下“停止”按钮。打开弹窗时,将通过 checkHasParsedLinks() 函数加载已保存的链接(希望我没有让您感到困惑)。弹窗
脚本中的事件监听器将完成所有操作。需要在弹出窗口打开时添加新链接。另一种选择是使用一个监听器来响应商店的变化(是的,商店的变化可以被监听),但在这种情况下,这样做不太合理。background.js
文件还包含一些新的代码:
JavaScript:复制到剪贴板
let currentURL, currentUrlObj
let parsedLinks = []
async function getCurrentURL(activeInfo) {
let activeTab = await browser.tabs.query({active:true, currentWindow: true})
currentURL = activeTab[0].url
currentUrlObj = new URL(currentURL)
return currentURL
}
async function searchLinks(text) {
let startLength = parsedLinks.length
let linkRegEx = /(?:"|')(((?:[a-zA-Z]{1,10}:\/\/|\/\/)[^"'/]{1,}\.[a-zA-Z]{2,}[^"']{0,})|((?:\/|\.\.\/|\.\/)[^"'><,;| *()(%%$^/\\\[\]][^"'><,;|()]{1,})|([a-zA-Z0-9_\-/]{1,}\/[a-zA-Z0-9_\-/]{1,}\.(?:[a-zA-Z]{1,4}|action)(?:[\?|/][^"|']{0,}|))|([a-zA-Z0-9_\-]{1,}\.(?:php|asp|aspx|jsp|json|action|html|js|txt|xml)(?:\?[^"|']{0,}|)))(?:"|')/ig
let results = text.match(linkRegEx)
if (!results) return
let allLinks = Array.from(new Set([...results, ...parsedLinks]))
let newLinks = allLinks.filter(el => !parsedLinks.includes(el))
parsedLinks = allLinks
if (parsedLinks.length > startLength)
browser.runtime.sendMessage({
action: 'response_links_update',
newLinks
})
}
function beforeRequestHandler(requestInfo) {
let filter = browser.webRequest.filterResponseData(requestInfo.requestId)
let decoder = new TextDecoder("utf-8")
filter.ondata = (event) => {
let str = decoder.decode(event.data, {stream: true});
searchLinks(str)
filter.write(event.data)
};
filter.onstop = (event) => {
filter.disconnect()
}
}
function saveData(hostname, key, object) {
browser.storage.local.get([hostname], result => {
browser.storage.local.set({[hostname]: {...result[hostname], [key]: object}})
})
}
browser.runtime.onInstalled.addListener(() => {
browser.tabs.onActivated.addListener(getCurrentURL)
browser.runtime.onMessage.addListener(onMessageHandler)
})
async function onMessageHandler(data) {
if (data.action == 'saveData' && data.key && data.object) {
if (typeof data.object == 'string')
data.object = JSON.parse(data.object)
return saveData(data.hostname, data.key, data.object)
} else if (data.action == 'getCurrentTabUrl') {
if (currentURL) return currentURL
return currentURL
} else if (data.action == 'getCookies') {
let cookies = await browser.cookies.getAll({
domain: data.hostname
})
checkCookies(data.hostname, cookies, saveData)
return false
} else if (data.action == 'checkMonitoring') {
let hasListener = browser.webRequest.onBeforeRequest.hasListener(beforeRequestHandler)
if (hasListener) return 'running'
return 'not running'
} else if (data.action == 'startMonitoring') {
parsedLinks = []
let urls = [
`${currentUrlObj.protocol}//${currentUrlObj.hostname}/*`,
`${currentUrlObj.protocol}//*.${currentUrlObj.hostname}/*`
]
await browser.webRequest.onBeforeRequest.addListener(beforeRequestHandler,
{
urls,
types: ['main_frame', 'script', 'xml_dtd', 'xmlhttprequest'],
},
['blocking', 'requestBody']
);
browser.tabs.query({active:true, currentWindow: true}).then(tabs => {
let tab = tabs[0]
browser.tabs.reload(tab.id, { bypassCache: true })
})
return 'running'
} else if (data.action == 'removeMonitoring') {
parsedLinks = []
let hasListener = browser.webRequest.onBeforeRequest.removeListener(beforeRequestHandler)
return hasListener
} else if (data.action == 'hasParsedLinks') {
return parsedLinks
}
return false
}
已解析链接数组已出现。借助它,重复项将被过滤掉,新的链接将被发送到弹出窗口以供添加。看一下searchLinks()。首先,我们记住已挖掘链接数组的大小。然后,我们将已挖掘的链接与新的链接批次合并,并使用Set删除重复项。接下来,我们只提取新值,丢弃parsedLinks中已有的所有内容,并将它们发送到弹出窗口。好了,我们将这些值保存在存储中。
注意事件监听器的添加、检查是否存在以及移除过程——所有地方都只使用函数名称。没有标识符,没有其他内容。下面我们将回到这个功能,为服务分配一个自定义解析器。
收集子域名
管理面板、开发者版本、Web 应用程序的 Beta 版本、phpMyAdmin……您可以在子域名中找到所有内容。例如,您可以在此主题中查看有关收集方法的更多信息。我建议使用多种服务,并从 crt.sh 开始,因为它操作简单。只需执行 GET 请求https://crt.sh/?q=domain.tld&output=json
即可 。由于这是对第三方资源的请求,因此为了避免干扰,我们从后台脚本执行该请求:
JavaScript:复制到剪贴板
async function getSubdomains(hostname, saveDataFunction) {
let domain = hostname.split('.').slice(-2).join('.')
let headers = new Headers({
"Content-Type" : "application/json",
"User-Agent" : navigator.userAgent
});
let response = await fetch(`https://crt.sh/?q=${domain}&output=json`, {
method: 'GET',
headers
})
let responseJson = await response.json()
let subdomains =Array.from(new Set(responseJson.map(item => item.common_name)))
saveDataFunction(hostname, 'subdomains', subdomains)
return subdomains
}
如果你已经读到这里,这段代码应该很容易理解。我们从服务中请求了 JSON 数据,清理并保存了它。我只想简单介绍一下域名。我们可以使用主机名,但效果不太好。CRT.sh 提供的信息比使用域名时少得多。所以我使用了最简单的钩子:我拆分了主机名,删除了除了域名和 TLD 之外的所有内容,然后重新组合。瞧,我们得到了一个干净的域名……为了输出
到弹出脚本,我们在检查中添加了另一项检查:
JavaScript:复制到剪贴板if (data.subdomains && data.subdomains.length) printSubdomains(data.subdomains)
以及将子域名添加为列表的函数:
JavaScript:复制到剪贴板
function printSubdomains(subdomains) {
let subdomainsList = document.querySelector('#subdomains-list-crt')
subdomains.forEach(subdomain => {
let li = document.createElement('li')
li.innerText = subdomain
subdomainsList.append(li)
})
}
我非常喜欢来自 censys、dnsinfo 和其他服务的数据。但是 WAF 和验证码的形式存在问题。这个问题可以通过不同的方式解决:尝试欺骗 CloudFlare、连接验证码猜测服务、直接访问 API、将包含验证码的页面加载给用户等等。在本文中,我将使用最愚蠢、最粗暴的方案:我将添加一个按钮,点击后会打开用户感兴趣的标签页。此外,我还将添加一个 dnsinfo 响应解析器作为示例。在此基础上,每个人都可以创建自己的解析器。
首先,我们将为弹出窗口中的按钮分配一个操作:
JavaScript:复制到剪贴板
document.querySelector('#loadDNSInfo').addEventListener('click', event => {
event.preventDefault()
browser.runtime.sendMessage({action: 'openDNSTabs', hostname: url.hostname})
})
因此,必须在后台脚本中添加一个处理程序。相关服务的链接将存储在一个数组中,并使用“$$$”替换域名:
JavaScript:复制到剪贴板
let domainCheckSites = [
`https://viewdns.info/reverseip/?host=$$$&t=1`,
`https://search.censys.io/search?resource=hosts&sort=RELEVANCE&per_page=25&virtual_hosts=EXCLUDE&q=$$$`,
`https://www.shodan.io/search?query=$$$`
]
else if (data.action == 'openDNSTabs') {
let domain = data.hostname.split('.').slice(-2).join('.')
domainCheckSites.forEach(link => {
let linkToOpen = link.replace('$$$', domain)
browser.tabs.create({
active: false,
url: linkToOpen
})
})
}
我们将利用 DNSInfo 做一些巧妙的事情。在打开标签页之前,我们将使用以下参数分配一个拦截器:
JavaScript:复制到剪贴板
else if (data.action == 'openDNSTabs') {
let domain = data.hostname.split('.').slice(-2).join('.')
parsedDNSInfoHost = data.hostname
browser.webRequest.onBeforeRequest.addListener(parseDNSInfoHandler,
{
urls: [`https://viewdns.info/reverseip/?host=${domain}*`],
types: ['main_frame'],
},
['blocking']
);
domainCheckSites.forEach(link => {
let linkToOpen = link.replace('$$$', domain)
browser.tabs.create({
active: false,
url: linkToOpen
})
})
}
因此,拦截器将仅响应 dnsinfo 中的特定搜索,并绑定到特定域。我们将处理“main_frame”,因为所有必要信息都会立即在响应中到达,无需任何额外下载。我们将记住主机名以保存数据。记住这一点是为了防止在收到信息之前切换选项卡。JavaScript
处理函数
:复制到剪贴板
function parseDNSInfoHandler(requestInfo) {
let filter = browser.webRequest.filterResponseData(requestInfo.requestId)
let decoder = new TextDecoder("utf-8")
filter.ondata = (event) => {
let str = decoder.decode(event.data, {stream: true});
let ipRegex = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
let tableRegex = /(?<=<br><br>)<table.*<\/table>/mi
let ip = str.match(ipRegex)[0]
let table = str.match(tableRegex)
if (table && table.length) {
saveData(parsedDNSInfoHost, 'dnsinfo', {
ip, table: btoa(table)
})
}
filter.write(event.data)
};
filter.onstop = (event) => {
filter.disconnect()
}
browser.webRequest.onBeforeRequest.removeListener(parseDNSInfoHandler)
}
在很多方面,它已经很熟悉了。分配过滤器后,我会立即删除事件监听器。为什么现在需要它呢?一旦触发过滤器,我们就能获取必要的数据,然后就不用管它了。
在流处理程序内部,有两个正则表达式。一个解析 IP,另一个解析表格。是的,为了避免麻烦,我直接使用整个表格。为了避免存储表格时出现问题,我将其编码为 base64。
输出所需的内容很少:
JavaScript:复制到剪贴板
function printDNSInfo({dnsinfo}){
document.querySelector('#dnsinfo-ip').innerText = dnsinfo.ip
document.querySelector('#dnsinfodata').innerHTML = atob(dnsinfo.table)
}
这是一种从服务中解析必要数据的方法,虽然有点棘手,但确实很棘手。虽然无法批量解析,但可以很好地针对特定对象进行操作。
JS API DNS接口
在扩展程序可用的 API 对象中,有一个“dns”。借助它,您可以获取一些信息。
首先,我将在清单中添加“dns”的权限。然后,我将转到空的 content.js(好吧,我们必须在其中添加一些内容)并添加:
JavaScript:复制到剪贴板
browser.runtime.sendMessage({
action: 'getDNSInfo',
hostname: window.location.hostname
})
因此,您需要向后台脚本添加相应事件的处理程序:
JavaScript:复制到剪贴板
else if (data.action == 'getDNSInfo') {
let response = await browser.dns.resolve(data.hostname, [
"allow_name_collisions",
"bypass_cache",
"canonical_name",
])
saveData(data.hostname, 'dns', response)
}
在这里,我们要求浏览器向我们提供以下信息:
- allow_name_collisions - 不过滤冲突。如果收到冲突的信息,浏览器会尝试过滤。让我们请求浏览器禁用此过滤器。
- bypass_cache - 禁用 DNS 查找缓存
- canonical_name - 请显示 CNAME
仍需添加输出:
JavaScript:复制到剪贴板
(async function() {
...
if (data.dns) printDns(data)
...
})()
function printDns({dns}) {
document.querySelector('#dnsCanonicalName').innerText = dns.canonicalName
document.querySelector('#dnsIP').innerHTML = `<li>${dns.addresses.join('</li><li>')}</li>`
}
结论
这篇文章很长,甚至可能有点冗长,尽管我尽可能地循序渐进地介绍和分析每个细节。
再次提醒您,虽然最终的扩展可以用于您的实际操作,但它们更应该被视为示例和概念介绍。每个具体案例都有很多细节需要确定。例如,保存数据。目前,保存操作由完整主机完成。在您的情况下是这样吗?也许按整个域来累积数据更好?接下来的问题是如何更改存储结构,以便于分析和输出信息。或者,例如,是否有必要将
所有解析后的数据保存在本地存储中?例如,某些解析结果可以存储在会话存储中。或者,为了跟踪网站上发生的更改,动态收集数据是否更有意义?
总的来说,我们讨论了几种数据解析类型:使用 DOM 功能进行解析、直接解析 HTML 页面代码以及解析附带文件(通过 webRequest 拦截的脚本)。我们还讨论了数据存储的选项。使用标签页、在扩展程序的不同部分之间转发消息等等。我计划再写 1-2 篇文章来介绍主动扫描和基于浏览器扩展程序创建自动漏洞利用的问题,但仅凭本文提供的信息,您已经可以自己轻松完成所有这些操作。
如果您有任何问题、需要说明的地方或建议,请随时在评论区留言。如果您对这个话题感兴趣,也请告诉我。
分享
你的反应是什么?






