资讯详情

WebMail发信交互监听:CDP+前端Hook深度追踪HTTP事务链

📅 2026/10/4 18:33:02 | 华诺云谱 👁 阅读
WebMail发信交互监听:CDP+前端Hook深度追踪HTTP事务链
简介本资源是一份面向网络安全与信息内容安全方向学习者的实践型实验报告聚焦WebMail发信交互过程的网络层监听与敏感信息提取适用于高校信息安全、网络工程专业学生及初级安全研究人员。报告基于Libnids开发包实现TCP流捕获与重组完整解析HTTP POST请求中的用户名、密码、收件人、发件人及邮件正文等明文数据深入展现WebMail通信协议特征与常见安全风险点。资源为1个602KB的Word文档.doc涵盖需求分析、Libnids初始化与TCP回调设计、Wireshark对比验证截图、关键代码注释及实验总结图文并茂逻辑清晰便于理解TCP分段识别、ACK字段驱动的流重组等核心技术细节。目前已有1214人学习下载是掌握网络入侵检测基础开发、协议解析与流量分析能力的典型教学案例。1. 为什么“监听WebMail发信交互过程”不是抓包那么简单它本质是逆向分析一个带状态、有防刷、含前端校验的HTTP事务链你打开网页邮箱点“发送”页面弹出“发送成功”但后端到底做了什么是直接调SMTP发信还是先存草稿再异步投递有没有触发反垃圾策略有没有走邮件网关二次鉴权这些光靠Wireshark抓到几个POST包根本说不清——因为现代WebMail如Outlook Web、Zimbra、Coremail、网易邮箱大师Web版早已把发信拆成多阶段交互前端表单校验 → CSRF Token获取 → 收件人自动补全API调用 → 附件分片上传 → 正文HTML净化 → 最终提交带签名的JSON Payload。所谓“监听发信交互过程”核心不是监听某个端口而是在浏览器上下文里完整捕获从用户点击到服务器返回200/400之间的全部HTTP请求-响应序列、DOM状态变更、JavaScript执行路径与关键变量值。它适用于安全审计人员查邮件外泄风险、运维排查发信延迟根因、开发调试新邮件模板兼容性或合规团队验证敏感词过滤是否生效。这不是网络层监听而是应用层事务追踪不依赖服务器权限却比单纯看Nginx日志精细十倍。如果你还在用Fiddler只截一个/api/send就下结论那90%的发信失败原因你永远看不到。2. 用Chrome DevTools ProtocolCDP实现无侵入式全程监听从启动浏览器到捕获完整事务链现代WebMail发信流程高度依赖JavaScript驱动传统代理工具如Charles、Fiddler无法捕获XHR被abort、Fetch被重试、Service Worker拦截等场景更无法关联DOM变化与网络请求。CDP是Chrome/Edge原生提供的调试协议能精确控制浏览器行为、注入脚本、监听事件、捕获所有网络请求及响应体——且无需修改目标页面代码真正实现“旁路监听”。2.1 启动带远程调试端口的Chrome实例并连接CDP# Linux/macOS启动无沙箱、禁用GPU、开放CDP端口的Chrome google-chrome --remote-debugging-port9222 \ --no-sandbox \ --disable-gpu \ --disable-extensions \ --disable-plugins \ --disable-dev-shm-usage \ --user-data-dir/tmp/chrome_dev_test提示--user-data-dir必须指定独立路径否则会复用默认配置导致登录态冲突或CDP连接失败。Windows用户请将google-chrome替换为chrome.exe路径需用双反斜杠或正斜杠。启动后访问http://localhost:9222/json可看到当前打开的页面列表返回类似[{ description: , devtoolsFrontendUrl: /devtools/devtools.html?wslocalhost:9222/devtools/page/..., id: A1B2C3D4..., title: Outlook - Inbox, type: page, url: https://outlook.office.com/mail/inbox, webSocketDebuggerUrl: ws://localhost:9222/devtools/page/A1B2C3D4... }]关键字段是webSocketDebuggerUrl这是后续建立CDP连接的WebSocket地址。2.2 使用Python pychrome捕获发信全过程的网络与DOM事件# install: pip install pychrome import pychrome import json import time from urllib.parse import urlparse # 连接CDP browser pychrome.Browser(urlhttp://127.0.0.1:9222) tab browser.list_tab()[0] # 获取第一个标签页即WebMail页面 tab.start() # 启用Network和DOM域 tab.Network.enable() tab.DOM.enable() # 定义请求捕获逻辑 request_log [] response_log [] dom_changes [] def on_request_will_be_sent(**kwargs): url kwargs.get(request, {}).get(url, ) if send in url.lower() or /api/ in url and (mail in url or message in url): request_log.append({ timestamp: time.time(), method: kwargs.get(request, {}).get(method), url: url, headers: kwargs.get(request, {}).get(headers, {}), postData: kwargs.get(request, {}).get(postData, None) }) def on_response_received(**kwargs): req_id kwargs.get(requestId) status kwargs.get(response, {}).get(status, 0) url kwargs.get(response, {}).get(url, ) if status in [200, 400, 403, 500] and any(k in url.lower() for k in [send, compose, message]): response_log.append({ requestId: req_id, status: status, url: url, headers: kwargs.get(response, {}).get(headers, {}), body_size: kwargs.get(response, {}).get(encodedDataLength, 0) }) def on_dom_content_event(**kwargs): # 监听DOM变化如发送按钮变灰、成功提示弹出 dom_changes.append({ timestamp: time.time(), event: DOMContentEvent, params: kwargs }) # 绑定事件回调 tab.Network.requestWillBeSent on_request_will_be_sent tab.Network.responseReceived on_response_received tab.DOM.setEventListener on_dom_content_event # 开始监听注意必须在用户操作前启用 tab.start() # 【关键】等待用户手动完成发信操作或注入JS自动触发 print(✅ 已启动监听。请在WebMail页面中完成一次发信操作写信→点击发送...) input(按回车键继续...) # 停止监听并导出日志 tab.stop() print(f\n 捕获到 {len(request_log)} 条疑似发信请求{len(response_log)} 条响应{len(dom_changes)} 次DOM变更)参数说明与逻辑要点on_request_will_be_sent在请求发出前触发可拿到原始请求头、POST Body若未被加密、CSRF Token等关键凭证on_response_received在响应到达时触发能捕获4xx/5xx错误码及响应头如X-Mail-Queue-ID、X-Spam-StatusDOM.setEventListener并非标准CDP方法实际应使用DOM.getDocumentDOM.querySelectorDOM.setChildNodes组合监听节点变化此处简化示意真实项目中建议用DOM.getNodes配合DOM.childNodeCountUpdated事件postData字段仅当请求体为text/plain或application/x-www-form-urlencoded时可见若为application/json需结合Network.getResponseBody主动拉取见2.3节input()阻塞是为确保人工操作与监听时间窗口对齐——自动化场景应改用tab.Page.navigate跳转tab.wait等待特定DOM元素出现。2.3 主动拉取JSON响应体与请求体绕过CDP默认不返回Body的限制CDP默认不返回请求/响应体避免内存爆炸需显式调用Network.getResponseBody和Network.getRequestPostData# 在on_response_received回调中追加 if kwargs.get(response, {}).get(status) 200: try: body tab.Network.getResponseBody(requestIdreq_id) response_log[-1][body] body.get(body, ) response_log[-1][base64Encoded] body.get(base64Encoded, False) except Exception as e: response_log[-1][body_error] str(e) # 在on_request_will_be_sent中追加仅对POST/PUT if kwargs.get(request, {}).get(method) in [POST, PUT]: try: post_data tab.Network.getRequestPostData(requestIdkwargs[requestId]) request_log[-1][postData] post_data.get(postData, ) except Exception as e: request_log[-1][postData_error] str(e)注意getRequestPostData仅对Content-Type为application/x-www-form-urlencoded或text/plain有效若为application/jsonCDP无法直接提取需改用Page.addScriptToEvaluateOnNewDocument注入脚本劫持fetch/XMLHttpRequest见第4章。3. 用前端Hook技术劫持fetch/XHR捕获被CDP遗漏的加密Payload与重试逻辑CDP虽强大但对以下场景束手无策WebMail使用fetchAPI且body为FormData对象CDP不解析二进制请求被Service Worker拦截并重写如离线队列、加密代理关键参数在JS运行时动态拼接如时间戳签名、AES加密正文发信失败后自动重试但重试请求未触发CDPrequestWillBeSent因被JS缓存或取消。此时必须在页面JS上下文中注入Hook直接监听网络调用原语。3.1 注入fetch Hook捕获所有fetch调用的原始参数与返回结果# 在tab.start()后、用户操作前执行 hook_js // 保存原始fetch const originalFetch window.fetch; // 重写fetch window.fetch async function(input, init {}) { // 记录请求信息注意input可能是URL字符串或Request对象 const url input instanceof Request ? input.url : input; const method (input instanceof Request ? input.method : init.method) || GET; // 尝试提取body仅处理常见类型 let body null; if (init.body) { if (typeof init.body string) { body init.body; } else if (init.body instanceof FormData) { // FormData需转换为对象仅支持文本字段 const formDataObj {}; for (let [k, v] of init.body.entries()) { if (typeof v string) formDataObj[k] v; } body JSON.stringify(formDataObj); } } // 发送前日志 console.log([WEBMAIL-HOOK] fetch start:, { url, method, body }); try { const response await originalFetch(input, init); // 读取响应体仅text避免blob阻塞 const textBody await response.clone().text(); console.log([WEBMAIL-HOOK] fetch success:, { url, status: response.status, body: textBody.length 2000 ? textBody : (too long) }); return response; } catch (err) { console.error([WEBMAIL-HOOK] fetch error:, { url, error: err.message }); throw err; } }; tab.Page.addScriptToEvaluateOnNewDocument(scriptSourcehook_js)关键细节addScriptToEvaluateOnNewDocument确保脚本在页面任何frame加载时都执行覆盖iframe内嵌的发信组件如腾讯企业邮的附件上传iframeFormData处理仅提取文本字段因二进制文件如图片无法安全转为JSON实际项目中应记录FormData.keys()并标注“含文件”response.clone().text()是安全读取响应体的方式避免消耗原始流若需完整二进制应改用arrayBuffer()并Base64编码日志输出到console需配合tab.Runtime.enable()tab.Runtime.consoleAPICalled监听才能捕获——这是CDP中易被忽略的联动步骤见3.2。3.2 捕获console日志并结构化归档把前端Hook输出转为可分析数据# 启用Runtime域并监听console输出 tab.Runtime.enable() console_logs [] def on_console_api_called(**kwargs): # 过滤WEBMAIL-HOOK日志 if args not in kwargs: return first_arg kwargs[args][0].get(value, ) if kwargs[args] else if isinstance(first_arg, str) and [WEBMAIL-HOOK] in first_arg: # 解析JSON-like字符串简化版 try: log_obj json.loads(first_arg.split(] , 1)[1]) console_logs.append({ timestamp: time.time(), type: hook, data: log_obj }) except: console_logs.append({ timestamp: time.time(), type: hook_raw, raw: first_arg }) tab.Runtime.consoleAPICalled on_console_api_called玄学经验某些WebMail如Zimbra会压缩console输出导致JSON.parse失败。此时应改用正则提取关键字段re.search(rurl:\s*([^,]),\s*status:\s*(\d), first_arg)。别硬刚JSON能提取字段就行。3.3 XHR Hook补充兼容老式WebMail与IE兼容模式尽管fetch已成主流但部分企业级WebMail如早期Coremail仍重度依赖XHR。需同时Hookhook_xhr_js const originalXHR window.XMLHttpRequest; window.XMLHttpRequest function() { const xhr new originalXHR(); const _open xhr.open; xhr.open function(method, url) { this._url url; this._method method; _open.apply(this, arguments); }; const _send xhr.send; xhr.send function(body) { console.log([WEBMAIL-HOOK] XHR send:, { url: this._url, method: this._method, body: typeof body string ? body : (binary) }); // 监听load事件获取响应 this.addEventListener(load, function() { console.log([WEBMAIL-HOOK] XHR load:, { url: this._url, status: this.status, response: this.responseText.length 2000 ? this.responseText : (too long) }); }); _send.apply(this, arguments); }; return xhr; }; tab.Page.addScriptToEvaluateOnNewDocument(scriptSourcehook_xhr_js)与fetch Hook的区别XHR需在send时记录请求在load事件中记录响应二者时间分离responseText可能为空如响应为application/json但未设置responseType此时需监听onreadystatechange并检查readyState 4不要尝试Hookonerror——它常被WebMail自身try-catch吞掉不可靠。4. 避坑WebMail发信监听的5个血泪经验——现象、原因与解决WebMail环境复杂度远超普通Web应用以下是在Outlook Web、网易邮箱、Zimbra实测中踩出的真坑每一条都附带可立即验证的解决方案。4.1 现象CDP捕获到/api/send请求但getPostData返回None且响应体为空原因该请求使用Content-Type: application/json且CDP未启用Network.setRequestInterception导致无法主动拉取Body同时WebMail可能对敏感字段如收件人邮箱做前端AES加密原始JSON已被混淆。解决启用请求拦截tab.Network.setRequestInterception(patterns[{urlPattern: *send*}])在Network.requestIntercepted事件中调用Network.getResponseBody需先continueInterceptedRequest若仍为空说明加密发生在JS层——此时必须用3.1节的fetch Hook并在Hook中console.log(JSON.stringify(init))查看原始参数。4.2 现象发信成功后CDP未捕获任何/api/send请求只看到一堆/api/autosave调用原因WebMail采用“草稿自动保存最终提交”两段式流程真正的发信请求由/api/submitDraft?idxxx触发且该ID在DOM中动态生成如input typehidden iddraftId valueabc123。解决在DOM.documentUpdated事件中扫描input[iddraftId]或[data-draft-id]属性或监听MutationObserver当#send-button变为disabled时立即DOM.querySelector获取draft ID将捕获逻辑从url contains send改为url contains submitDraft or commit。4.3 现象Hook脚本注入后WebMail页面白屏或报SecurityError: Failed to execute replaceState原因WebMail使用history.pushState管理路由而Hook脚本在document_start阶段执行早于React/Vue初始化导致路由库检测到history.state被篡改而崩溃。解决将addScriptToEvaluateOnNewDocument的runAt参数设为document_idle默认为document_start或改用Page.addScriptToEvaluateOnNewDocumentsetTimeout(() { /* hook code */ }, 100)延迟执行最稳妥方案监听window.addEventListener(load, ...)后再注入。4.4 现象附件上传请求被捕获但postData显示[object FormData]无法看到文件名与内容原因FormData对象在CDP中序列化为[object FormData]其内部字段不可直接访问且大文件上传常走分片如/api/upload/chunk单次请求不包含完整信息。解决在fetch Hook中对init.body instanceof FormData遍历entries()并记录key与value.toString()文件名可通过value.name获取对分片上传监听/upload/chunk系列请求按chunkIndex和totalChunks参数拼接还原关键字段如fileName、fileSize、contentType必记它们常出现在FormData.append(file, blob, report.pdf)的第三个参数。4.5 现象同一发信操作CDP捕获3次/api/send但只有最后一次返回200前两次是401或429原因WebMail实现“Token续期限流重试”机制首次请求因CSRF Token过期返回401触发前端刷新Token第二次因QPS超限返回429触发指数退避重试。解决不要只看第一个200响应需按requestId关联整个请求链在responseReceived中记录response.headers[X-RateLimit-Remaining]和response.headers[X-CSRF-Token]构建请求图谱requestId→initiator是other还是script →redirectResponse→fromCache识别重试路径。5. 进阶技巧构建可回放的发信事务快照——用Puppeteer录制CDP重放验证业务逻辑监听只是第一步真正价值在于复现与验证。比如发现某次发信因X-Spam-Status: Yes被拒你想确认是前端漏传X-Mail-Priority头还是后端规则误判此时需要“重放”该次请求而非重新手工操作。5.1 从CDP日志提取可执行的curl命令自动生成调试脚本def generate_curl_from_cdp(log_entry): 从CDP request_log条目生成curl命令 req log_entry[request] url req[url] method req[method] headers req[headers] data log_entry.get(postData, ) curl_cmd fcurl -X {method} {url} \\\n # 添加headers for k, v in headers.items(): if k.lower() not in [content-length, host, cookie]: # 过滤冗余头 curl_cmd f -H {k}: {v} \\\n # 添加data if data and method in [POST, PUT]: if \n in data or len(data) 100: # 写入临时文件 tmp_file f/tmp/curl_data_{int(time.time())}.json with open(tmp_file, w) as f: f.write(data) curl_cmd f -d {tmp_file} else: curl_cmd f -d {data} return curl_cmd.strip(\\\n) # 示例对最后一条send请求生成curl if request_log: last_send [r for r in request_log if send in r[url].lower()][-1] print(generate_curl_from_cdp(last_send))输出示例curl -X POST https://mail.example.com/api/v1/messages/send \ -H X-CSRF-Token: abc123 \ -H Content-Type: application/json \ -d { to: [userdomain.com], subject: Test, body: pHello/p }后悔药生成的curl可直接粘贴终端调试或集成进Postman Collection。注意X-CSRF-Token有时效性重放前需重新获取。5.2 用Puppeteer重放事务注入原始CookieHeaders绕过登录态校验CDP监听得到的Cookie常包含HttpOnly字段无法通过document.cookie读取但Puppeteer可直接设置const puppeteer require(puppeteer); (async () { const browser await puppeteer.launch({ headless: false }); const page await browser.newPage(); // 设置原始Cookie从CDP Network.responseReceived.headers[set-cookie]提取 const cookies [ { name: JSESSIONID, value: ABC123, domain: mail.example.com, path: /, httpOnly: true }, { name: XSRF-TOKEN, value: xyz789, domain: mail.example.com, path: / } ]; await page.setCookie(...cookies); // 设置请求头 await page.setUserAgent(Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36); await page.setExtraHTTPHeaders({ X-CSRF-Token: xyz789, X-Requested-With: XMLHttpRequest }); // 访问发信页面非登录页直击发信接口 await page.goto(https://mail.example.com/mail/compose, { waitUntil: networkidle0 }); // 执行原始请求用page.evaluate注入fetch const result await page.evaluate((url, payload) { return fetch(url, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload) }).then(r r.json()); }, https://mail.example.com/api/send, { to: [testdomain.com], subject: Replay Test, body: pFrom replay/p }); console.log(Replay result:, result); await browser.close(); })();关键点setCookie支持httpOnly: true完美复现服务端下发的会话CookiesetExtraHTTPHeaders全局设置避免每次fetch都写page.evaluate中执行fetch确保在目标页面上下文运行能访问页面JS环境如加密函数若发信需调用页面JS函数如window.mailClient.send()可直接await page.evaluate(() window.mailClient.send(...))。5.3 构建事务快照JSON Schema定义可交换、可审计的监听结果格式为让监听结果能被安全团队、运维、开发共同消费我定义了最小可行快照结构已用于3个企业邮箱审计项目{ snapshot_id: webmail-send-20240520-142301-abc123, target_url: https://outlook.office.com/mail/inbox, start_time: 2024-05-20T14:23:01.123Z, end_time: 2024-05-20T14:23:15.456Z, user_action: click_send_button, requests: [ { sequence: 1, url: https://mail.example.com/api/drafts/auto, method: POST, status: 200, request_headers: { X-CSRF-Token: ... }, request_body: { subject: Test, body: pHi/p }, response_headers: { X-Draft-ID: draft_789 }, response_body: { id: draft_789 } }, { sequence: 2, url: https://mail.example.com/api/messages/send, method: POST, status: 200, request_headers: { X-CSRF-Token: ..., X-Draft-ID: draft_789 }, request_body: { draftId: draft_789, sendNow: true }, response_headers: { X-Mail-Queue-ID: q_123456 }, response_body: { status: queued, queueId: q_123456 } } ], dom_events: [ { timestamp: 2024-05-20T14:23:08.789Z, event: button_disabled, selector: #send-button, value: true }, { timestamp: 2024-05-20T14:23:12.345Z, event: toast_shown, message: 发送成功, duration_ms: 3000 } ], security_checks: { has_spf_dkim_dmarc: true, contains_sensitive_keywords: false, attachment_scanned: true } }为什么这个Schema管用sequence字段明确请求时序避免“哪个请求触发了哪个响应”的歧义security_checks是预留字段可接入本地敏感词引擎或调用邮件网关API验证SPF/DKIM结果所有时间戳用ISO 8601便于ELK日志系统聚合request_body和response_body为JSON对象非原始字符串方便下游用jq或Python直接解析。我坚持在每个监听脚本末尾加一行json.dump(snapshot, open(fsnapshot_{int(time.time())}.json, w))不是为了存档而是强迫自己把“看到的”转化为“可验证的”。有一次客户坚称“发信没走我们自己的邮件网关”我甩出快照里X-Mail-Gateway: internal-smtp的header对方运维当场重启了配置同步服务。这种时刻你会觉得写几十行CDP代码值了。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑