├── .DS_Store ├── README.md ├── background.js ├── icons ├── .DS_Store ├── icon-48.png └── icon-96.png ├── manifest.json ├── popup ├── popup.css ├── popup.html └── popup.js ├── test.html └── test.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuy0ung/Niffler/e8cc8cd1d5532775cdca2e6a6899c4b92d7a47a8/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 嗅嗅-Niffler 2 | 3 | ## 维护中... 4 | 5 | 功能点开发: 6 | 7 | - [x] 插件基础开发 8 | - [x] 被动识别 9 | - [x] 拦截 10 | - [x] 消息提醒 11 | - [x] 弹窗 12 | - [x] 分网页拦截 13 | - [x] 白名单 14 | - [x] 拦截算法优化(针对主域名而不是所有) 15 | - [ ] 拦截精准性优化 16 | - [ ] 自动更新 17 | 18 | 有其他需求可以提issue哦(^_^) 19 | ## 简介 20 | 21 | 嗅嗅是一款运行在firefox上的jsonp蜜罐被动扫描插件 22 | 23 | (工具可能影响日常网页感受,建议只在进行渗透测试时开启插件) 24 | 25 | Niffler is a JSONP honeypot passive scanner that runs on Firefox. 26 | (The tool may affect your regular web browsing experience. It is recommended to enable the add-on only during penetration testing.) 27 | 28 | ![](https://yuy0ung.oss-cn-chengdu.aliyuncs.com/icon-96.png) 29 | 30 | 安装好扩展后,可以在浏览器左上角打开它: 31 | 32 | ![image-20250318163945729](https://yuy0ung.oss-cn-chengdu.aliyuncs.com/image-20250318163945729.png) 33 | 34 | 运行后,嗅嗅可以在后台检测当前网页发起的跨站请求,若具有jsonp接口特征的请求或对一些媒体网站请求数量过大,嗅嗅会对网站的请求进行拦截并发起告警: 35 | 36 | * 弹窗告警: 37 | ![328a1bd4d2595c2e5515af4552aee14b](https://yuy0ung.oss-cn-chengdu.aliyuncs.com/328a1bd4d2595c2e5515af4552aee14b.png) 38 | 39 | * 消息告警: 40 | 41 | ![image-20250318162931625](https://yuy0ung.oss-cn-chengdu.aliyuncs.com/image-20250318162931625.png) 42 | 43 | ## 插件安装 44 | 打开浏览器临时扩展管理界面:about:debugging#/runtime/this-firefox 45 | 找到“临时加载附加组件” 添加下载的插件文件夹 46 | image 47 | 最后固定到工具栏中即可使用! 48 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // 域名敏感名单 4 | const DOMAIN_KEYWORDS = [ 5 | '1616', '163', '51cto', '58pic', 'alicdn', 'amap', 'apple', 'baidu', 'bilibili', 6 | 'bit', 'c-ctrip', 'chinaunix', 'cnblogs', 'cndns', 'cnzz', 'com', 'comgithub', 7 | 'csdn', 'ctfile', 'ctrip', 'dangdang', 'faloo', 'fastadmin', 'github', 'gnu', 8 | 'growingio', 'hupu', 'huya', 'ifeng', 'ip', 'iqiyi', 'iqiyipic', 'iteye', 'itpub', 9 | 'jd', 'jiyoujia', 'mgtv', 'mop', 'mths', 'pptv', 'qq', 'qy', 'renren', 10 | 'scorecardresearch', 'sitestar', 'skylink', 'sogou', 'sohu', 'sougou', 'taihe', 11 | 'thatscaptaintoyou', 'tianya', 'uc', 'weibo', 'youku', 'zbj', 'zhibo8' 12 | ]; 13 | 14 | // JSONP特征参数 15 | const JSONP_KEYWORDS = ['callback', 'jsonp', 'cb', 'function', 'token', 'auth']; 16 | 17 | // 域名白名单(支持通配符) 18 | const WHITE_LIST = [ 19 | '*.qq.com', 20 | '*.163.com', 21 | '*.baidu.com', 22 | '*.bilibili.com', 23 | '*.cnblogs.com', 24 | '*.weibo.com', 25 | '*.jd.com', 26 | '*.taobao.com', 27 | '*.csdn.net', 28 | '*.google.com', 29 | '*.bing.com', 30 | '*.moonshot.com', 31 | '*.deepseek.com', 32 | '*.n.com', 33 | '*.github.com' 34 | ]; 35 | 36 | // 预编译白名单正则表达式 37 | const WHITE_LIST_REGEX = new RegExp( 38 | WHITE_LIST.map(domain => { 39 | if (domain.startsWith('*.')) { 40 | return `(^|\\.)${domain.slice(2).replace(/\./g, '\\.')}$`; 41 | } 42 | return `^${domain.replace(/\./g, '\\.')}$`; 43 | }).join('|'), 44 | 'i' 45 | ); 46 | 47 | let isEnabled = true; 48 | const tabStates = new Map(); 49 | 50 | // 白名单检测 51 | function isWhitelisted(hostname) { 52 | return WHITE_LIST_REGEX.test(hostname); 53 | } 54 | 55 | // 初始化标签页状态 56 | function initTabState(tabId) { 57 | if (!tabStates.has(tabId)) { 58 | tabStates.set(tabId, { 59 | counter: 0, 60 | isLocked: false, 61 | hasAlerted: false, // 新增弹窗状态标记 62 | lockedHosts: new Set(), 63 | timer: null 64 | }); 65 | } 66 | return tabStates.get(tabId); 67 | } 68 | 69 | // 蜜罐警报(优化单次触发逻辑) 70 | function triggerHoneypotAlert(finding) { 71 | const state = tabStates.get(finding.tabId); 72 | if (!state || state.hasAlerted) return; // 防止重复触发 73 | 74 | state.hasAlerted = true; // 标记已触发 75 | 76 | chrome.storage.sync.get(["alerts"], result => { 77 | // 页面弹窗(仅触发一次) 78 | if (result.alerts !== false) { 79 | chrome.tabs.executeScript(finding.tabId, { 80 | code: `alert('疑似蜜罐: 检测到 ${finding.key} (特征:${finding.match}) 来自 ${finding.src}');` 81 | }); 82 | } 83 | 84 | // 系统通知(同样仅一次) 85 | chrome.notifications.create({ 86 | type: 'basic', 87 | iconUrl: chrome.runtime.getURL('icons/icon-128.png'), 88 | title: `嗅嗅闻到了蜜罐的味道 | ${finding.key}`, 89 | message: `${finding.match.substring(0,24)}... @ ${finding.src.replace(/^www\./, '')}`, 90 | priority: 2 91 | }); 92 | }); 93 | } 94 | 95 | // 主拦截逻辑(修改触发条件) 96 | chrome.webRequest.onBeforeRequest.addListener( 97 | function(details) { 98 | if (!isEnabled || details.tabId === -1) return; 99 | 100 | try { 101 | const originUrl = new URL(details.originUrl || details.url); 102 | if (isWhitelisted(originUrl.hostname)) return { cancel: false }; 103 | } catch (e) { 104 | console.warn('URL解析失败:', e); 105 | return { cancel: false }; 106 | } 107 | 108 | const tabId = details.tabId; 109 | const state = initTabState(tabId); 110 | const currentUrl = new URL(details.url); 111 | 112 | // 已锁定直接拦截(不重复弹窗) 113 | if (state.isLocked && state.lockedHosts.has(currentUrl.hostname)) { 114 | return { cancel: true }; 115 | } 116 | 117 | // 特征检测(保持不变) 118 | const isCrossDomain = !details.url.startsWith(details.originUrl); 119 | const queryParams = Array.from(currentUrl.searchParams.keys()); 120 | const isJsonp = queryParams.some(p => 121 | JSONP_KEYWORDS.some(k => p.toLowerCase().includes(k)) 122 | ); 123 | const isSuspDomain = DOMAIN_KEYWORDS.some(k => 124 | currentUrl.hostname.includes(k) 125 | ); 126 | 127 | if (isCrossDomain && (isJsonp || isSuspDomain)) { 128 | state.counter++; 129 | 130 | // 初始化计时器(保持不变) 131 | if (!state.timer) { 132 | state.timer = setTimeout(() => { 133 | state.counter = 0; 134 | state.timer = null; 135 | }, 8000); 136 | } 137 | 138 | // 关键修改:仅首次达到阈值时触发 139 | if (state.counter === 4) { // 使用严格等于判断 140 | const detectedType = isJsonp ? 'JSONP参数' : '域名特征'; 141 | const detectedValue = isJsonp 142 | ? queryParams.find(p => JSONP_KEYWORDS.some(k => p.includes(k))) 143 | : DOMAIN_KEYWORDS.find(k => currentUrl.hostname.includes(k)); 144 | 145 | triggerHoneypotAlert({ 146 | key: `${detectedType}命中`, 147 | match: detectedValue, 148 | src: currentUrl.hostname, 149 | tabId: tabId 150 | }); 151 | 152 | state.isLocked = true; 153 | state.lockedHosts.add(currentUrl.hostname); 154 | chrome.tabs.sendMessage(tabId, { 155 | action: "lockdown", 156 | blockedHost: currentUrl.hostname 157 | }); 158 | } 159 | 160 | return { cancel: state.isLocked }; 161 | } 162 | return { cancel: false }; 163 | }, 164 | { urls: [""] }, 165 | ["blocking"] 166 | ); 167 | 168 | // 消息监听 169 | chrome.runtime.onMessage.addListener((msg, _sender) => { 170 | if (msg.action === "toggle") { 171 | isEnabled = msg.state; 172 | chrome.storage.local.set({ enabled: msg.state }); 173 | } 174 | 175 | if (msg.type === 'scriptCheck') { 176 | try { 177 | const url = new URL(msg.url); 178 | if (JSONP_KEYWORDS.some(k => url.searchParams.has(k))) { 179 | triggerHoneypotAlert({ 180 | key: "动态脚本检测", 181 | match: "可疑参数", 182 | src: url.hostname, 183 | tabId: msg.tabId 184 | }); 185 | return true; 186 | } 187 | } catch (e) { 188 | console.warn('脚本检测URL解析失败:', e); 189 | } 190 | } 191 | }); 192 | 193 | // 标签页清理 194 | chrome.tabs.onRemoved.addListener(tabId => tabStates.delete(tabId)); 195 | 196 | // 初始化 197 | chrome.storage.local.get(['enabled'], res => { 198 | isEnabled = res.enabled ?? true; 199 | }); 200 | 201 | // 调试模式 202 | if (process.env.NODE_ENV === 'development') { 203 | chrome.runtime.onMessage.addListener((msg, _sender) => { 204 | if (msg.action === 'debug') { 205 | console.log('当前标签页状态:', tabStates); 206 | } 207 | }); 208 | } -------------------------------------------------------------------------------- /icons/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuy0ung/Niffler/e8cc8cd1d5532775cdca2e6a6899c4b92d7a47a8/icons/.DS_Store -------------------------------------------------------------------------------- /icons/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuy0ung/Niffler/e8cc8cd1d5532775cdca2e6a6899c4b92d7a47a8/icons/icon-48.png -------------------------------------------------------------------------------- /icons/icon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuy0ung/Niffler/e8cc8cd1d5532775cdca2e6a6899c4b92d7a47a8/icons/icon-96.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Niffler - JSONP蜜罐嗅探器", 4 | "version": "1.0", 5 | "description": "嗅探并阻断JSONP蜜罐的嗅嗅神器", 6 | "icons": { 7 | "48": "icons/icon-48.png", 8 | "96": "icons/icon-96.png" 9 | }, 10 | "permissions": [ 11 | "webRequest", 12 | "webRequestBlocking", 13 | "storage", 14 | "activeTab", 15 | "alarms", 16 | "notifications", 17 | "tabs", 18 | "" 19 | ], 20 | "browser_action": { 21 | "default_icon": { 22 | "48": "icons/icon-48.png", 23 | "96": "icons/icon-96.png" 24 | }, 25 | "default_title": "Niffler", 26 | "default_popup": "popup/popup.html" 27 | }, 28 | "background": { 29 | "scripts": ["background.js"], 30 | "persistent": true 31 | }, 32 | "web_accessible_resources": [ 33 | "icons/*" 34 | ], 35 | "browser_specific_settings": { 36 | "gecko": { 37 | "id": "niffler@hogwarts", 38 | "strict_min_version": "78.0" 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /popup/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 300px; 3 | background: #2c1a0f; 4 | color: #ffd700; 5 | font-family: 'Cinzel', serif; 6 | } 7 | 8 | .hogwarts-container { 9 | padding: 20px; 10 | background: url('data:image/png;base64,iVBORw0...') repeat; 11 | border: 3px solid #5c4033; 12 | box-shadow: 0 0 15px #ffd700; 13 | } 14 | 15 | .marauders-title { 16 | font-size: 1.8em; 17 | text-align: center; 18 | text-shadow: 2px 2px #5c4033; 19 | margin-bottom: 20px; 20 | } 21 | 22 | .toggle-container { 23 | display: flex; 24 | align-items: center; 25 | justify-content: space-between; 26 | margin: 15px 0; 27 | } 28 | 29 | /* 优化开关样式 */ 30 | .switch { 31 | position: relative; 32 | display: inline-block; 33 | width: 60px; 34 | height: 34px; 35 | } 36 | 37 | /* 隐藏原生checkbox */ 38 | .switch input { 39 | opacity: 0; 40 | width: 0; 41 | height: 0; 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | right: 0; 46 | bottom: 0; 47 | z-index: 2; /* 确保点击区域覆盖整个开关 */ 48 | } 49 | 50 | .slider { 51 | position: absolute; 52 | cursor: pointer; 53 | top: 0; 54 | left: 0; 55 | right: 0; 56 | bottom: 0; 57 | background-color: #2c1a0f; 58 | transition: .4s; 59 | border-radius: 34px; 60 | border: 2px solid #c0a464; 61 | box-shadow: 0 0 5px rgba(255, 215, 0, 0.5); 62 | } 63 | 64 | .slider:before { 65 | position: absolute; 66 | content: ""; 67 | height: 26px; 68 | width: 26px; 69 | top:2px; 70 | left: 2px; 71 | bottom: 4px; 72 | background-color: #c0a464; 73 | transition: .15s; 74 | border-radius: 50%; 75 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3); 76 | } 77 | 78 | input:checked + .slider { 79 | background-color: #5c4033; 80 | border-color: #ffd700; 81 | } 82 | 83 | input:checked + .slider:before { 84 | transform: translateX(26px); 85 | background-color: #ffd700; 86 | } 87 | 88 | /* 添加悬停效果 */ 89 | .switch:hover .slider { 90 | box-shadow: 0 0 8px #c0a464; 91 | } 92 | 93 | .switch:hover input:checked + .slider { 94 | box-shadow: 0 0 8px #ffd700; 95 | } 96 | 97 | -------------------------------------------------------------------------------- /popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

嗅嗅-Niffler

10 |
11 | 启动蜜罐嗅探 12 | 16 |
17 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /popup/popup.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | const toggle = document.getElementById('toggle'); 3 | 4 | // 同步初始状态 5 | chrome.storage.local.get(['enabled'], result => { 6 | const enabled = result.enabled ?? true; 7 | toggle.checked = enabled; 8 | // 立即通知后台 9 | chrome.runtime.sendMessage({ action: "toggle", state: enabled }); 10 | }); 11 | 12 | toggle.addEventListener('change', function() { 13 | const isChecked = this.checked; 14 | chrome.storage.local.set({ enabled: isChecked }, () => { 15 | // 带错误处理的通信 16 | chrome.runtime.sendMessage( 17 | { action: "toggle", state: isChecked }, 18 | (response) => { 19 | if (chrome.runtime.lastError) { 20 | console.error('状态同步失败:', chrome.runtime.lastError); 21 | toggle.checked = !isChecked; // 回滚UI状态 22 | } 23 | } 24 | ); 25 | }); 26 | }); 27 | }); -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Niffler 插件测试页面 6 | 51 | 52 | 53 |

Niffler 插件测试页面

54 | 55 |
56 |

测试用例

57 | 58 |
59 | 60 | 61 | 62 | 63 | 64 |
65 | 66 |

检测结果:

67 |
68 |
69 | 70 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuy0ung/Niffler/e8cc8cd1d5532775cdca2e6a6899c4b92d7a47a8/test.js --------------------------------------------------------------------------------