简介:本资源是一份面向网络安全与信息内容安全方向学习者的实践型实验报告,聚焦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 Protocol(CDP)实现无侵入式全程监听:从启动浏览器到捕获完整事务链
现代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-port=9222 \ --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?ws=localhost: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(url="http://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-Status);DOM.setEventListener并非标准CDP方法,实际应使用DOM.getDocument+DOM.querySelector+DOM.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(requestId=req_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(requestId=kwargs['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/json,CDP无法直接提取,需改用Page.addScriptToEvaluateOnNewDocument注入脚本劫持fetch/XMLHttpRequest(见第4章)。
3. 用前端Hook技术劫持fetch/XHR:捕获被CDP遗漏的加密Payload与重试逻辑
CDP虽强大,但对以下场景束手无策:
- WebMail使用
fetchAPI且body为FormData对象(CDP不解析二进制); - 请求被Service Worker拦截并重写(如离线队列、加密代理);
- 关键参数在JS运行时动态拼接(如时间戳签名、AES加密正文);
- 发信失败后自动重试,但重试请求未触发CDP
requestWillBeSent(因被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(scriptSource=hook_js)关键细节:
addScriptToEvaluateOnNewDocument确保脚本在页面任何frame加载时都执行,覆盖iframe内嵌的发信组件(如腾讯企业邮的附件上传iframe);FormData处理仅提取文本字段,因二进制文件(如图片)无法安全转为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(r'url:\s*([^,]+),\s*status:\s*(\d+)', first_arg)。别硬刚JSON,能提取字段就行。
3.3 XHR Hook补充:兼容老式WebMail与IE兼容模式
尽管fetch已成主流,但部分企业级WebMail(如早期Coremail)仍重度依赖XHR。需同时Hook:
hook_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(scriptSource=hook_xhr_js)与fetch Hook的区别:
- XHR需在
send时记录请求,在load事件中记录响应,二者时间分离; responseText可能为空(如响应为application/json但未设置responseType),此时需监听onreadystatechange并检查readyState === 4;- 不要尝试Hook
onerror——它常被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?id=xxx触发,且该ID在DOM中动态生成(如<input type="hidden" id="draftId" value="abc123">)。
解决:
- 在
DOM.documentUpdated事件中扫描input[id="draftId"]或[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.addScriptToEvaluateOnNewDocument+setTimeout(() => { /* 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 = f"curl -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": ["user@domain.com"], "subject": "Test", "body": "<p>Hello</p>" }'后悔药:生成的curl可直接粘贴终端调试,或集成进Postman Collection。注意
X-CSRF-Token有时效性,重放前需重新获取。
5.2 用Puppeteer重放事务:注入原始Cookie+Headers,绕过登录态校验
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: ['test@domain.com'], subject: 'Replay Test', body: '<p>From replay</p>' }); console.log('Replay result:', result); await browser.close(); })();关键点:
setCookie支持httpOnly: true,完美复现服务端下发的会话Cookie;setExtraHTTPHeaders全局设置,避免每次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": "<p>Hi</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(f'snapshot_{int(time.time())}.json', 'w')),不是为了存档,而是强迫自己把“看到的”转化为“可验证的”。有一次,客户坚称“发信没走我们自己的邮件网关”,我甩出快照里X-Mail-Gateway: internal-smtp的header,对方运维当场重启了配置同步服务。这种时刻,你会觉得写几十行CDP代码,值了。
希望帮到你。
本文还有配套的精品资源,点击获取