【某数WAF 动态Cookie实战】
某数WAF 动态Cookie绕过
- JS RPC与MITM协同实战 🛡️
- 缘起:动态Cookie的挑战 🤔
- 核心思路:JSRPC + MITM 💡
- 实现细节 🛠️
- 1. 浏览器控制台注入脚本1:Cookie捕获与展示
- 2. 浏览器控制台注入脚本2:JS RPC 客户端 (Hlclient)
- 3. MITM 替换脚本 (Python with mitmproxy)
- 部署与使用步骤 🚀
- 总结与展望 🌟
JS RPC与MITM协同实战 🛡️
在现代Web渗透测试中,动态刷新的Cookie和前端JavaScript混淆是常见的棘手问题。前者使得传统的请求重放变得困难,后者则增加了分析客户端逻辑的复杂度。本文将介绍一种结合浏览器端脚本注入、JavaScript RPC (JS RPC)以及中间人代理 (MITM) 技术的组合拳,有效应对此类挑战,并实现对关键数据包中Cookie的动态替换。
缘起:动态Cookie的挑战 🤔
在对某网站进行安全测试时,遇到了一个典型场景:
- Cookie实时刷新:用户的每次关键点击或交互都会导致服务器下发新的Cookie值。这意味着之前捕获的包含旧Cookie的数据包一旦重放,就会因为Cookie失效而请求失败。
- 前端JS混淆:网站的前端JavaScript代码经过了高度混淆,直接分析JS逻辑以理解Cookie生成和更新机制变得非常耗时且低效。
为了能够顺利进行渗透测试,需要一种方法来获取浏览器当前最新的Cookie,并在重放数据包时将其动态替换进去。传统的手动复制粘贴显然无法满足自动化测试的需求。
核心思路:JSRPC + MITM 💡
解决方案围绕以下三个核心组件构建:
- 浏览器端脚本注入 (Browser-Side Scripting):在浏览器控制台注入JS脚本,用于劫持Cookie的设置 (
document.cookie
) 和页面跳转相关API。当Cookie更新时,脚本捕获最新的Cookie值,并提供一种机制供外部调用。 - JavaScript RPC (JS RPC):利用JS RPC框架( jxhczhl/JsRpc ),在浏览器端运行一个RPC客户端,它通过WebSocket与本地启动的RPC服务端通信。这允许外部程序(如MITM脚本)远程调用浏览器环境中的JS函数。
- MITM代理脚本 (Man-in-the-Middle Proxy):使用
mitmproxy
等工具,编写Python脚本拦截流经代理的HTTP请求。当检测到目标站点的请求时,MITM脚本通过JS RPC从浏览器获取最新的Cookie,然后替换请求头中的旧Cookie,再将请求发往服务器。
调用关系图 (Mermaid):
实现细节 🛠️
1. 浏览器控制台注入脚本1:Cookie捕获与展示
此脚本的核心功能是:
- 劫持
document.cookie
的setter:当网站JS设置Cookie时,脚本1会先捕获到这个值。 - 提取纯净Cookie:通常
document.cookie = "key=value; path=/; domain=..."
这样设置,其实只用只关心key=value
部分。 - 存储最新Cookie:将提取到的纯净Cookie存入全局变量
window.cookieValueToDisplay
,方便后续RPC调用。 - 原始Cookie设置:通过创建一个临时的
iframe
来确保原始的Cookie设置逻辑仍然执行,避免破坏网站正常功能。 - 劫持导航API:覆盖
window.location.assign
,window.location.replace
,window.location.href
的setter,HTMLFormElement.prototype.submit
以及window.open
,在页面即将跳转前强制弹出Cookie展示框,确保在跳转导致Cookie可能变化或丢失前能捕获到最后的值。 - 自定义弹窗:创建一个浮层显示捕获到的Cookie,并提供点击复制功能。这里使用了
navigator.clipboard.writeText
进行无感复制。
(function() {const originalCookieDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');window.cookieValueToDisplay = ''; // 用于存储最新的 Cookie 值// --- 辅助函数:复制到剪贴板 (不带弹窗) ---async function copyToClipboard(text) {try {await navigator.clipboard.writeText(text);console.log('%c[Cookie Catcher] Cookie 已复制到剪贴板。', 'color: #4CAF50;');} catch (err) {console.error('%c[Cookie Catcher] 无法复制到剪贴板:', 'color: #ff0000;', err);// Fallback for older browsers or if Clipboard API failsconst textarea = document.getElementById('cookieTextarea');if (textarea) {textarea.select();try {document.execCommand('copy');console.log('%c[Cookie Catcher] Cookie 已复制到剪贴板 (使用 fallback 方式)。', 'color: #4CAF50;');} catch (execErr) {console.error('%c[Cookie Catcher] 无法复制到剪贴板。请手动复制。', 'color: #ff0000;');}} else {console.error('%c[Cookie Catcher] 无法复制到剪贴板。请手动复制。', 'color: #ff0000;');}}}// --- 辅助函数:提取纯净的 Cookie 值 ---function extractPureCookie(cookieString) {// 使用正则表达式匹配第一个分号(;)之前的内容,或者整个字符串如果没有分号const match = cookieString.match(/^([^;]+)/);return match ? match[1].trim() : cookieString.trim();}// --- 劫持 document.cookie setter ---Object.defineProperty(document, 'cookie', {configurable: true,enumerable: true,get: function() {return originalCookieDescriptor.get.call(this);},set: function(val) {console.log('%c[Cookie Catcher] Detected cookie set:', 'color: #1a73e8; font-weight: bold;', val);const pureCookie = extractPureCookie(val); // 提取纯净的 CookiecookieValueToDisplay = pureCookie; // 更新要显示的 Cookie 值// 1. 确保原始的 cookie 仍然被设置 (使用 iframe 方式)try {const existingTempDiv = document.getElementById('tempCookieSetterDiv');if (existingTempDiv) {document.body.removeChild(existingTempDiv);}var tempDiv = document.createElement('div');tempDiv.id = 'tempCookieSetterDiv';tempDiv.style.display = 'none';document.body.appendChild(tempDiv);// 为了确保原始 cookie 属性被设置,这里仍然使用原始的 valconst escapedVal = val.replace(/\\/g, '\\\\').replace(/'/g, "\\'");tempDiv.innerHTML = `<iframe srcdoc="<script>document.cookie='${escapedVal}';</script>"></iframe>`;setTimeout(() => {if (document.body.contains(tempDiv)) {document.body.removeChild(tempDiv);}}, 100);} catch (e) {console.error('%c[Cookie Catcher] Error setting original cookie via iframe:', 'color: #ff0000;', e);}// !!! 当 Cookie 被设置时,立即尝试显示弹窗 !!!showCookiePopup(cookieValueToDisplay);}});// --- 劫持页面跳转相关 API ---// 1. 劫持 window.location.assign / replace 方法const originalLocationAssign = window.location.assign;const originalLocationReplace = window.location.replace;window.location.assign = function(url) {console.warn('%c[Cookie Catcher] Location.assign detected! Value:', 'color: #ff9800;', url);showCookiePopup(cookieValueToDisplay); // 强制弹窗originalLocationAssign.call(this, url); // 继续原始跳转};window.location.replace = function(url) {console.warn('%c[Cookie Catcher] Location.replace detected! Value:', 'color: #ff9800;', url);showCookiePopup(cookieValueToDisplay); // 强制弹窗originalLocationReplace.call(this, url); // 继续原始跳转};// 2. 劫持 window.location.href 的赋值操作try {const originalHrefDescriptor = Object.getOwnPropertyDescriptor(window.location, 'href');if (originalHrefDescriptor && originalHrefDescriptor.set) {Object.defineProperty(window.location, 'href', {configurable: true,enumerable: true,get: function() {return originalHrefDescriptor.get.call(this);},set: function(url) {console.warn('%c[Cookie Catcher] Location.href set detected! Value:', 'color: #ff9800;', url);showCookiePopup(cookieValueToDisplay); // 强制弹窗originalHrefDescriptor.set.call(this, url); // 继续原始跳转}});} else {console.warn('%c[Cookie Catcher] Could not successfully hook window.location.href setter.', 'color: #ff9800;');}} catch (e) {console.error('%c[Cookie Catcher] Error hooking window.location.href:', 'color: #ff0000;', e);}// 3. 劫持 form.submit() (提交表单也可能导致跳转)const originalFormSubmit = HTMLFormElement.prototype.submit;HTMLFormElement.prototype.submit = function() {console.warn('%c[Cookie Catcher] Form submission detected!', 'color: #ff9800;', this);showCookiePopup(cookieValueToDisplay); // 强制弹窗originalFormSubmit.call(this); // 继续原始提交};// 4. 劫持 window.open() (如果目标是当前页面,会跳转)const originalWindowOpen = window.open;window.open = function(url, name, features) {console.warn('%c[Cookie Catcher] Window.open detected! Value:', 'color: #ff9800;', url);// 如果 window.open 目标是 "_self" 或未指定且是当前窗口,则会跳转if (name === '_self' || !name || name === window.name) {showCookiePopup(cookieValueToDisplay); // 强制弹窗}return originalWindowOpen.apply(this, arguments);};// --- 创建自定义弹窗的函数 ---function showCookiePopup(cookieString) {const oldPopup = document.getElementById('cookieCatcherPopup');if (oldPopup) {// 如果已有弹窗,只更新内容,不重新创建document.getElementById('cookieTextarea').value = cookieString;document.getElementById('cookiePopupTitle').textContent = '最新捕获的 Cookie (更新)';return;}const popup = document.createElement('div');popup.id = 'cookieCatcherPopup';popup.style.cssText = `position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);background-color: #f8f8f8;border: 2px solid #333;padding: 20px;box-shadow: 0 4px 8px rgba(0,0,0,0.2);z-index: 99999;font-family: monospace;max-width: 80vw;max-height: 80vh;overflow: auto;word-break: break-all;display: flex;flex-direction: column;gap: 10px;border-radius: 8px;pointer-events: all; /* 确保可以点击和复制 */`;const title = document.createElement('h3');title.id = 'cookiePopupTitle';title.textContent = '最新捕获的 Cookie';title.style.cssText = `margin: 0 0 10px 0;color: #333;font-size: 1.2em;`;const textarea = document.createElement('textarea');textarea.id = 'cookieTextarea';textarea.value = cookieString;textarea.rows = Math.min(10, Math.max(3, Math.ceil(cookieString.length / 80)));textarea.readOnly = true;textarea.style.cssText = `width: calc(100% - 4px);flex-grow: 1;padding: 10px;border: 1px solid #ccc;resize: vertical;font-size: 0.9em;background-color: #fff;color: #000;cursor: text;`;// 使用新的复制函数,不再弹窗textarea.onclick = function() {this.select(); // 仍然选中以便用户手动复制copyToClipboard(this.value);};const copyButton = document.createElement('button');copyButton.textContent = '点击此处或文本框复制';copyButton.style.cssText = `padding: 8px 15px;background-color: #4CAF50;color: white;border: none;border-radius: 5px;cursor: pointer;font-size: 1em;margin-top: 10px;`;copyButton.id = 'copyButtonme';// 使用新的复制函数,不再弹窗copyButton.onclick = function() {const textareaValue = document.getElementById('cookieTextarea').value;copyToClipboard(textareaValue);};const closeButton = document.createElement('button');closeButton.textContent = '关闭';closeButton.style.cssText = `padding: 8px 15px;background-color: #f44336;color: white;border: none;border-radius: 5px;cursor: pointer;font-size: 1em;`;closeButton.onclick = function() {document.body.removeChild(popup);};popup.appendChild(title);popup.appendChild(textarea);popup.appendChild(copyButton);popup.appendChild(closeButton);document.body.appendChild(popup);textarea.focus();textarea.select();}console.log('%c[Cookie Catcher] Cookie setter and navigation APIs hooked. Waiting for updates...', 'color: #2196F3; font-weight: bold;');
})();for (let i = 0; i < 115; i++) {const copyButton = document.getElementById('cookieCatcherPopup').querySelector('button');if (copyButton) {copyButton.click();console.log(`第 ${i + 1} 次模拟点击复制按钮`);// 每次点击后,浏览器可能会显示一个“已复制到剪贴板!”的alert,您需要手动关闭它才能继续下一次点击。// 在实际自动化中,会使用更高级的方法来处理alert,但此处是手动模拟。}
}
2. 浏览器控制台注入脚本2:JS RPC 客户端 (Hlclient)
此脚本在浏览器端建立一个WebSocket连接到本地的JS RPC服务端,并注册可供远程调用的函数。
Hlclient(wsURL)
构造函数:初始化WebSocket连接。connect()
方法:负责建立和重连WebSocket。regAction(func_name, func)
方法:注册一个动作。当RPC服务端收到特定action
的请求时,会调用浏览器端注册的对应func
。handlerRequest(requestJson)
方法:处理从服务端通过WebSocket收到的消息,解析JSON,找到对应的action
处理器并执行。sendResult(action, e)
方法:将执行结果通过WebSocket回传给服务端。transjson(formdata)
函数:一个辅助函数,用于处理可能非标准的JSON字符串。- 关键注册:
demo.regAction("getData", function (resolve, param){ eval(param); res=window.cookieValueToDisplay; resolve(res);})
- 注册了一个名为
getData
的动作。 - 当被调用时,它首先会执行传入的
param
字符串(通过eval(param)
)。这提供了一定的灵活性,例如param
可以是"document.getElementById('copyButtonme').click()"
来模拟点击,或者其他需要在获取Cookie前执行的JS代码。 - 然后,它获取全局变量
window.cookieValueToDisplay
(由脚本1更新)的值。 - 最后,通过
resolve(res)
将Cookie值返回给RPC服务端。
- 注册了一个名为
function Hlclient(wsURL) {// 检查传入的wsURL是否为空if (!wsURL) { throw new Error('wsURL can not be empty!!'); }this.wsURL = wsURL;this.handlers = {_execjs: function (resolve, param) {// 使用eval执行传入的JavaScript字符串var res = eval(param);// 如果没有返回值,则返回默认消息if (!res) { resolve("没有返回值"); } else { resolve(res); }}};this.socket = undefined;// 初始化连接this.connect();
}Hlclient.prototype.connect = function () {console.log('begin of connect to wsURL: ' + this.wsURL);var _this = this;try {// 创建WebSocket实例并绑定事件处理函数this.socket = new WebSocket(this.wsURL);this.socket.onmessage = function (e) { _this.handlerRequest(e.data); };} catch (e) {console.log("connection failed, reconnect after 10s");// 连接失败后,延迟10秒重试setTimeout(function () { _this.connect(); }, 10000);}// 监听socket关闭事件,尝试重新连接this.socket.onclose = function () { console.log('rpc已关闭');setTimeout(function () { _this.connect(); }, 10000);};// 添加open和error事件监听器this.socket.addEventListener('open', (event) => { console.log("rpc连接成功"); });this.socket.addEventListener('error', (event) => { console.error('rpc连接出错,请检查是否打开服务端:', event.error); });
};// 发送消息到服务器
Hlclient.prototype.send = function (msg) {this.socket.send(msg);
};// 注册新的动作处理器
Hlclient.prototype.regAction = function (func_name, func) {if (typeof func_name !== 'string') { throw new Error("an func_name must be string"); }if (typeof func !== 'function') { throw new Error("must be function"); }console.log("register func_name: " + func_name);this.handlers[func_name] = func;return true;
};// 处理从服务器收到的消息
Hlclient.prototype.handlerRequest = function (requestJson) {var _this = this;try {var result = JSON.parse(requestJson);} catch (error) {console.log("catch error", requestJson);result = transjson(requestJson);}if (!result['action']) {this.sendResult('', 'need request param {action}');return;}var action = result["action"];var theHandler = this.handlers[action];if (!theHandler) {this.sendResult(action, 'action not found');return;}try {if (!result["param"]) {theHandler(function (response) {_this.sendResult(action, response);});return;}var param = result["param"];try {param = JSON.parse(param);} catch (e) {}theHandler(function (response) {_this.sendResult(action, response);}, param);} catch (e) { console.log("error: " + e); _this.sendResult(action, e); }
};// 发送结果回服务器
Hlclient.prototype.sendResult = function (action, e) {if (typeof e === 'object' && e !== null) {try { e = JSON.stringify(e); } catch (v) {console.log(v); // 不是JSON无需操作}}this.send(action + atob("aGxeX14") + e);
};// 将非标准JSON字符串转换为对象
function transjson(formdata) {var regex = /"action":(?<actionName>.*?),/g;var actionName = regex.exec(formdata).groups.actionName;stringfystring = formdata.match(/{..data..:.*..\w+..:\s...*?..}/g).pop();stringfystring = stringfystring.replace(/\\"/g, '"');paramstring = JSON.parse(stringfystring);tens = `{"action":` + actionName + `,"param":{}}`;tjson = JSON.parse(tens);tjson.param = paramstring;return tjson;
}// 示例:创建一个Hlclient实例
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=zzz");
demo.regAction("getData",function (resolve,param){//document.getElementById('copyButtonme').click(); eval(param); res=window.cookieValueToDisplay;resolve(res);})
3. MITM 替换脚本 (Python with mitmproxy)
此Python脚本配合mitmproxy
工具使用,拦截HTTP请求,并通过JS RPC获取最新Cookie来替换请求头。
TARGET_HOST
: 定义目标网站域名。STATIC_EXTENSIONS
: 定义静态资源后缀,避免对这些资源也进行Cookie替换处理。rpc()
函数:- 向本地JS RPC服务端 (例如
http://localhost:12080/go
) 发起HTTP GET请求。 - 请求参数中指定了
group
,name
,action
(“getData”) 和param
。这里的param
是希望在浏览器RPC客户端执行的JS代码字符串,例如document.getElementById('copyButtonme').click()
。 - JS RPC服务端会将此
action
和param
通过WebSocket转发给浏览器端的Hlclient
。 Hlclient
执行后返回Cookie,rpc()
函数最终返回这个Cookie。
- 向本地JS RPC服务端 (例如
is_static_resource()
函数: 判断请求是否为静态资源。request(flow: http.HTTPFlow)
函数:mitmproxy
的核心处理函数。- 如果请求是静态资源或非目标主机,则跳过。
- 如果请求头中包含
Cookie
:- 调用
rpc()
获取浏览器端最新的Cookie (rpc_str
)。 - 可以预设一个基础Cookie (
Cookie ="FSSBBIl1UgzbN7NO=..."
),然后将RPC获取的动态部分追加或替换进去。在示例中,是直接拼接。更稳妥的做法是解析现有Cookie,替换掉动态变化的部分。 - 用
final_str
替换原始请求头中的Cookie
。
- 调用
from mitmproxy import http
import os
import requests
# -*- coding:utf-8 -*-# 目标主机
TARGET_HOST = "www.example.com"# 静态资源后缀列表(根据需要可继续添加)
STATIC_EXTENSIONS = [".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".svg",".ico", ".woff", ".woff2", ".ttf", ".eot", ".otf", ".map", ".mp4", ".webm"
]def rpc():url = "http://localhost:12080/go"params = {"group": "zzz","name": "hlg","action": "getData","param": "document.getElementById('copyButtonme').click()"}res = requests.get(url, params=params)return res.json()["data"]def is_static_resource(path: str) -> bool:_, ext = os.path.splitext(path.lower())return ext in STATIC_EXTENSIONSdef request(flow: http.HTTPFlow):# 排除静态资源if is_static_resource(flow.request.path):return# 仅处理目标 host 的请求if TARGET_HOST in flow.request.pretty_host:if "cookie" in flow.request.headers:Cookie ="FSSBBIl1UgzbN7NO=60fkObZ5wotjKLbDtdwWiwavQA5TRgIPjuw128_3ZCtcwiSjtWIswZjagalTr2h0ok_x_A5bQA7Xk2nW_qdy1GrA;"rpc_str = rpc()final_str = f"{Cookie} {rpc_str}"old_cookie = flow.request.headers["cookie"]flow.request.headers["cookie"] = final_strprint(f"[+] Replaced Cookie for {flow.request.pretty_url}")print(f" Old: {old_cookie}")print(f" New: {final_str}")
启动mitmproxy:
mitmproxy -s replace_cookie.py --listen-port 7777
部署与使用步骤 🚀
-
启动JS RPC服务端:根据 jxhczhl/JsRpc 项目说明,下载并运行其服务端。确保WebSocket服务 (如
ws://127.0.0.1:12080/ws
) 和HTTP接口 (如http://localhost:12080/go
) 正常工作。修改MITM脚本中的RPC_SERVER_URL
和RPC_PARAMS
以匹配RPC服务端配置。
-
运行MITM代理脚本:执行
mitmproxy -s replace_cookie.py --listen-port 7777
。配置浏览器或系统代理指向mitmproxy
监听的地址(默认为localhost:7777
)。 -
浏览器注入脚本:
- 访问目标网站。
- 打开浏览器开发者工具(通常是F12)。
- 在控制台(Console)中,先粘贴并执行脚本1 (Cookie捕获与展示)。
- 接着,粘贴并执行脚本2 (JS RPC客户端)。确保其中的WebSocket URL (
ws://127.0.0.1:12080/ws?group=zzz
) 与之前的RPC服务端和MITM脚本配置一致。
-
开始测试:正常浏览目标网站。当网站JS更新Cookie时,脚本1会捕获它。配置
Burpsuite
下一跳代理为localhost:7777
通过Burpsuite
发出请求到目标主机时,MITM脚本会通过RPC从浏览器获取最新的Cookie并替换到请求中。
总结与展望 🌟
通过结合浏览器端脚本注入、JS RPC和MITM代理,成功地构建了一套自动化获取并替换动态Cookie的解决方案。这种方法:
- 绕过了JS混淆:无需深入分析混淆的JS代码,直接从运行时获取结果。
- 实现了动态替换:确保了每次请求都使用当前浏览器中最新的有效Cookie。
- 提高了测试效率:为后续的自动化渗透测试工具(如SQLMap、Burp Intruder等配合上游代理)提供了便利。
竟然还往下翻,好了,复杂的方法你学会了,是时候告诉你点简单的方法了,参考项目:
- https://github.com/R0A1NG/Botgate_bypass
- https://github.com/wjlin0/riverPass
虽然做了点无用功,但至少累着了,至少下次针对加解密有法子了,哈哈哈哈