Piw*_*oli 5 javascript xss google-chrome csrf content-security-policy
我遇到过一个带有广告横幅的网站。当广告拦截或类似内容删除横幅元素时,网站会重新发送alerts有关无法加载广告的垃圾邮件,从而使观看体验比广告更糟糕。
我喜欢修补并尝试打破网站上的这些“保护”,但是,此时我没有想法。
该网站似乎总体上采用了广泛的保护措施来防止篡改,因此,虽然这个问题“简单地”是关于在头部完成对象冻结的操作,但如何做到这一点存在多种限制。
首先,我不想通过对横幅做一些特殊的事情来解决这个问题,例如使它们可见而不是删除它们。我想攻击alert.
问题是,当网站加载其 时index.html,它会冻结window.alert该部分中的对象,<head>如下所示:
<script>
Object.defineProperty(window, 'alert', {
configurable: false,
writable: false
})
</script>
Run Code Online (Sandbox Code Playgroud)
之后就不可能这样做了,例如:
window.alert = null;
鉴于我们无法触及警报本身,人们可能会考虑尝试查找并删除对警报的调用......
...不幸的是,这似乎也是不可能的,因为如果没有 JavaScript,网站将无法工作,并且网站使用CSRF-token加载其余脚本。所有加载的脚本还在每个请求中使用 CSRF 令牌。
该网站最初在头部加载一个“加载器脚本”,并且它受到原始策略的保护,因此无法从其他任何地方提供该脚本。该加载器脚本本身使用 CSRF 令牌通过 XMLHttpRequest 加载其余脚本。
还有内容安全策略标头。
这实质上意味着不可能例如让网络服务器充当代理来加载网页、剥离头部中的冻结脚本并传递修改后的网页。至少据我所知 - 据我所知,这正是 CSRF 令牌所阻止的。
我正在使用一个名为Resource Override的 Chrome 扩展,它可以让我:
对我来说,显而易见的第一步就是简单地将 a 注入window.alert = null到<head>网站的 中,但这不起作用,因为当页面和页面的 html 加载时,对象冻结已经存在于头部中。
此外,这种方法永远行不通,因为只有当存在要注入脚本的页面时才能将脚本注入到页面中,但在这种情况下,当这样的页面存在时,对象冻结代码也已经存在。
Stackoverflow 上有许多关于在 JavaScript 中解冻/解冻冻结对象的问题和解答。总之,不可能。至少在某种程度上没有帮助。
老实说,我对原型污染攻击的应用不太熟悉,但我确实对它们进行了一些研究,因为我觉得摆弄可以以__proto__某种方式让我使可window.alert配置或可写。看来这也不是办法。
我用我所掌握的知识尝试过的一件事是简单地扩展Object.prototype,它是所有对象的“根原型”,具有名为alert的属性,因此:
Object.prototype.alert = 999;
Run Code Online (Sandbox Code Playgroud)
它的作用是,当你现在做这样的事情时:
const arr = [1, 2, 3];
const str = "abc";
for(prop in arr) {
console.log(prop) // will log alert at some point
}
console.log(str.alert) // will log 999
Run Code Online (Sandbox Code Playgroud)
有趣的是,正确获取对象的所有可枚举属性名称(包括原型链(警报)中的属性名称)的唯一方法似乎是使用所示的循环for...in。所有其他方法,例如方便的命名方法Object.getOwnPropertyNames,只会直接返回对象的属性,而不返回其原型链。可以在此处找到相关文档。
无论如何,这个想法是:
A)幸运的话,也以某种方式覆盖 window.alert,因为 window 也是一个对象,因此应该由 Object.prototype.x 扩展
B)幸运的话,希望网站window.alert以某种可笑的方式调用,以便发出警报-所有东西的属性都会把事情搞砸
不出所料,这两件事都没有发生。
我也玩过代理,最初的理解是它可以让我“挂钩”到函数调用中,这样我就可以拦截对警报函数本身的调用。但这不是代理的工作方式,所以吸取了教训。
不过,它确实提出了一个问题,是否可以通过其他方式拦截/监听 JavaScript 中的函数调用。显然,我可以用我自己的函数包装任何函数,但随后所有内容都需要调用包装函数而不是原始函数,这在这里没有帮助。
还有一个相当于“Reflection API”的 JavaScript,但从它的外观和我所做的一些测试来看,它也没有提供任何有关挂钩/监听/拦截/重定向函数调用的信息,尽管文档中指出那:
Reflect 是一个内置对象,提供可拦截 JavaScript 操作的方法。
由 Chrome 本地覆盖修改的脚本不会通过某种完整性检查,因此它们将被阻止。
像往常一样,我尝试了各种不同的想法,例如:
Object.freeze = null,尽管在它可以设置为 null 或无论如何修改时它已经被调用了在我看来,一切皆有可能,尽管这种情况下的解决方案可能会很复杂。我想我已经用尽了大部分快速简单的方法,但我希望我错过了一些东西。
我仍在进行的一个想法是,首先尝试拦截和剥离内容安全策略标头,以更好地允许拦截和修改获取脚本的请求。我也不确定这最终是否会起作用,因为似乎还正在进行某种完整性验证。
有趣的是,似乎没有针对内联脚本的政策,所以这些政策确实有效。正因为如此,我对基于 JavaScript 的解决方案最感兴趣,但我会接受任何东西。
我也很感兴趣是否有一种方法可以在加载网站之前运行 JavaScript 或设置“修改的 JavaScript 上下文”?我想人们可以对 V8 进行修改并删除其核心的整个警报功能,但我正在寻找比这稍微少一点的东西。
window.alert由于它的广泛性,我不会对此感到惊讶,但至少我写得很开心。我发现这真的很有趣,一个看似良好的网站及其特定功能(例如警报功能)现在可以防止篡改 - 调查这一点让我大开眼界。
即便如此,需要使用多少种不同的措施还是有点可笑的;来源检查、完整性检查、散列内联脚本、内容安全策略、CSRF 令牌、以非常特定的方式和在非常特定的位置加载的脚本……所有这些都是为了防止我在某种意义上将一个小警报功能设置为 null !
如果从这一切中得出一个结论,那就是一个巨大的问题:为什么不能简单地阻止网站发送警报?各种功能都有权限控制,为什么警报和提示不是其中之一呢?
就像我在自己的回答中所说的那样,事实证明该网站的防篡改措施比我预期的还要广泛,因此我在此添加了我事后挖掘的内容。即使设法使警报对象可写,它仍然不能那么容易地被替换,因为这会触发额外的显式防篡改检查。
在我的回答中,我将绕过所有这些检查,但这就是我们正在处理的内容:
tamperCheck()
{
for (const check of [
{ob: Function.prototype, fun: 'toString'},
{ob: window, fun: 'alert'},
{ob: window, fun: 'confirm'},
{ob: Object, fun: 'freeze'},
{ob: Object, fun: 'seal'},
{ob: Object, fun: 'getPrototypeOf'},
{ob: Object, fun: 'getOwnPropertyDescriptors'},
]) {
const regex = new RegExp('^function ' + check.fun + '\\(\\)\\s*{\\s*\\[native code]\\s*}$');
let d;
try {
d = delete check.ob[check.fun]['toString'];
} catch {
d = false;
}
if (
!d
|| typeof Object.getPrototypeOf(check.ob[check.fun]) !== 'function'
|| typeof check.ob[check.fun] !== 'function'
|| check.ob[check.fun].name !== check.fun
|| check.ob[check.fun].toString().replaceAll('\n', '').match(regex) === null
) {
return true;
}
Object.defineProperty(check.ob, check.fun, {value: check.ob[check.fun], configurable: false, writable: false});
}
return false;
}
Run Code Online (Sandbox Code Playgroud)
好吧,在打败这个东西之前我无法入睡,所以我就这么做了。这个解决方案结果非常复杂,希望有更容易和更简单的解决方案,但这是我的。
此解决方案需要制作 chrome 扩展。具体来说,清单 V2 扩展。不幸的是,Manifest V2 扩展将在 2023 年不再受支持,正如我的扩展中的错误所示:
Manifest 版本 2 已弃用,并且将于 2023 年删除支持。有关更多详细信息,请参阅https://developer.chrome.com/blog/mv2-transition/ 。
但目前这有效。我对 chrome 扩展不太熟悉,所以这可能也可以通过 Manifest V3 扩展来完成。
该扩展可以做很多事情:
run_at: "document_start"内容脚本选项尽早执行脚本的某些部分(感谢@evolutionxbox)<script>在执行之前拦截静态节点window.alertwindow.alert防篡改检查的修补程序当我切换到 Manifest V2 时,这个问题非常简单。我无法让它与 Manifest V3 扩展一起使用。我们使用declarativeNetRequest API 让扩展修改 content-security-policy 标头。
manifest.json 需要以下属性(除了通常的属性之外):
{
"permissions": [
"declarativeNetRequest",
"declarativeNetRequestWithHostAccess",
"declarativeNetRequestFeedback",
"*://<website.domain>/*"
],
"declarative_net_request" : {
"rule_resources" : [
{
"id": "ruleset_1",
"enabled": true,
"path": "rules.json"
}
]
}
}
Run Code Online (Sandbox Code Playgroud)
rules.json 看起来像这样:
[
{
"id" : 1,
"priority": 1,
"action" : {
"type" : "modifyHeaders",
"responseHeaders": [
{ "header": "content-security-policy", "operation": "set", "value": "default-src * data: blob: filesystem: about: ws: wss: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; " }
]
},
"condition" : {
"urlFilter": "*://*/*",
"resourceTypes": ["main_frame", "sub_frame", "script", "other"]
}
}
]
Run Code Online (Sandbox Code Playgroud)
这样的manifest.json与rules.json相结合使得扩展可以修改content-security-policy标头,以便可以将内联脚本注入到页面中。这里的文档解释了为什么我们显然必须使用 V2 清单版本。
要在 document_start 处运行扩展,manifest.json 需要具有以下选项块:
"content_scripts": [
{
"matches": ["<website>"],
"js": ["inject.js"],
"run_at": "document_start"
}
]
Run Code Online (Sandbox Code Playgroud)
这将使扩展在 document_start 处执行inject.js 中的任何内容,因此当文档尚未加载时。
<script>节点创建现在我们的脚本可以足够早地执行,因此在文档尚未加载之前,我们可以设置一个 MutationObserver 来拦截<script>网页中静态服务的节点添加。
这就是inject.js 的开头:
class ScriptInterceptor {
#observer
#config = { childList: true, subtree: true };
#handlerFn
constructor(handlerFn) {
this.#handlerFn = handlerFn;
this.#observer = new MutationObserver(this.#callback.bind(this));
this.#observer.observe(document, this.#config);
}
#isScriptMutation = (node) => {
return node.tagName === "SCRIPT";
}
#callback(mutationList, observer) {
for(const mutation of mutationList) {
for(const node of mutation.addedNodes) {
if(this.#isScriptMutation(node)) {
this.#handlerFn(node);
}
}
}
}
}
const scriptTrace = "Object.defineProperty(window,'alert',{configurable:false,writable:false})";
const interceptor = new ScriptInterceptor((script) => {
if(script.textContent.includes(scriptTrace)) {
script.textContent = "";
script.remove();
}
});
Run Code Online (Sandbox Code Playgroud)
那么这里发生了什么?我创建了一个 ScriptInterceptor 类,它在内部创建一个 MutationObserver 实例来监视整个文档中元素子列表或子树的任何更改。
当 MutationObserver 注意到一个或多个 DOM 突变时,我们会检查每个突变和受影响的 DOM 节点,同时过滤掉所有不是节点的内容<script>。
当我们遇到<script>节点时,我们调用处理函数并将找到的节点传递给它。在这种情况下,我们的处理函数如下所示:
(script) => {
if(script.textContent.includes(scriptTrace)) {
script.textContent = "";
script.remove();
}
}
Run Code Online (Sandbox Code Playgroud)
其中是冻结对象的scriptTrace特定元素的代码内容:<script>window.alert
const scriptTrace = "Object.defineProperty(window,'alert',{configurable:false,writable:false})";
Run Code Online (Sandbox Code Playgroud)
这消除了对象的冻结,但我们仍然没有对其本身进行任何操作window.alert来禁用它。
<script>关于 MutationObserver 和静态服务块的旁注所以我在这里经常使用静态这个术语,这是有原因的。此 MutationObserver 方法不适用于动态添加到页面的脚本,例如以下脚本:
const dynamicScript = document.createElement('script');
dynamicScript.text = `(() => { console.log("Hello!") })()`;
document.documentElement.appendChild(dynamicScript);
Run Code Online (Sandbox Code Playgroud)
虽然 MutationObserver 会捕获它们被添加到页面中,但它只会在脚本执行后才会这样做。
我的情况不需要处理这样的拦截脚本,但如果有人发现自己处于这种情况,我确实出于兴趣找到了一种解决方案:
Element.prototype.appendChild = () => { };
Element.prototype.append = () => { };
Element.prototype.prepend = () => { };
Element.prototype.after = () => { };
Element.prototype.before = () => { };
Element.prototype.insertAdjacentElement = () => { };
Element.prototype.insertAdjacentHTML = () => { };
Element.prototype.insertAdjacentText = () => { };
Element.prototype.replaceChildren = () => { };
Element.prototype.replaceWith = () => { };
Element.prototype.setHTML = () => { };
Object.defineProperty(Element.prototype, "appendChild", { configurable:false, writable:false });
Object.defineProperty(Element.prototype, "append", { configurable:false, writable:false });
Object.defineProperty(Element.prototype, "prepend", { configurable:false, writable:false });
Object.defineProperty(Element.prototype, "after", { configurable:false, writable:false });
Object.defineProperty(Element.prototype, "before", { configurable:false, writable:false });
Object.defineProperty(Element.prototype, "insertAdjacentElement", { configurable:false, writable:false });
Object.defineProperty(Element.prototype, "insertAdjacentHTML", { configurable:false, writable:false });
Object.defineProperty(Element.prototype, "insertAdjacentText", { configurable:false, writable:false });
Object.defineProperty(Element.prototype, "replaceChildren", { configurable:false, writable:false });
Object.defineProperty(Element.prototype, "replaceWith", { configurable:false, writable:false });
Object.defineProperty(Element.prototype, "setHTML", { configurable:false, writable:false });
Run Code Online (Sandbox Code Playgroud)
这只是一个样板,但这里的想法是,我们实际上可以覆盖所有可能用于将<script>标签插入页面的方法,并由它们自己锁定它们。重点不是将方法留空,而是在那里实现某种逻辑来捕获并取消感兴趣的动态脚本的插入,并使其他一切正常工作。偏离主题,但可能很高兴知道。
window.alert并使其无法检测到ject.js 脚本的其余部分如下所示:
const inject = () => {
const fn = () => {
// Alert actor
const alert = () => { }
alert.toString = () => { return "function alert() { [native code] }" }
const antiTamperCheckFaker = {
deleteProperty() {
return true;
},
getPrototypeOf() {
return () => { };
}
}
const alertProxy = new Proxy(alert, antiTamperCheckFaker);
window.alert = alertProxy;
Object.defineProperty(window, 'alert', { configurable: false, writable: false });
}
const e = document.createElement('script');
e.text = `(${fn.toString()})()`;
document.documentElement.appendChild(e);
}
inject();
Run Code Online (Sandbox Code Playgroud)
我们在这里所做的事情本质上很简单——我们正在变成window.alert一个不执行任何操作的函数() => { }。
然而,当我到达这一点时,网站开始变砖,因为window.alert网页中的一些防篡改检查检测到了我的补丁。深入挖掘,我发现了以下篡改检查功能(我也将编辑原始问题):
tamperCheck()
{
for (const check of [
{ob: Function.prototype, fun: 'toString'},
{ob: window, fun: 'alert'},
{ob: window, fun: 'confirm'},
{ob: Object, fun: 'freeze'},
{ob: Object, fun: 'seal'},
{ob: Object, fun: 'getPrototypeOf'},
{ob: Object, fun: 'getOwnPropertyDescriptors'},
]) {
const regex = new RegExp('^function ' + check.fun + '\\(\\)\\s*{\\s*\\[native code]\\s*}$');
let d;
try {
d = delete check.ob[check.fun]['toString'];
} catch {
d = false;
}
if (
!d
|| typeof Object.getPrototypeOf(check.ob[check.fun]) !== 'function'
|| typeof check.ob[check.fun] !== 'function'
|| check.ob[check.fun].name !== check.fun
|| check.ob[check.fun].toString().replaceAll('\n', '').match(regex) === null
) {
return true;
}
Object.defineProperty(check.ob, check.fun, {value: check.ob[check.fun], configurable: false, writable: false});
}
return false;
}
Run Code Online (Sandbox Code Playgroud)
它看起来有点混乱,但如果我们这样说,我们可以让事情变得更简单:
check.ob = window和check.fun = alert
首先,我们可以将check.funRegExp 行中的 替换为“alert”:
const regex = new RegExp('^function alert \\(\\)\\s*{\\s*\\[native code]\\s*}$');
Run Code Online (Sandbox Code Playgroud)
目前该正则表达式尚未实际使用,但我会回过头来讨论它。接下来我们有 try...catch 块:
let d;
try {
d = delete check.ob[check.fun]['toString'];
} catch {
d = false;
}
Run Code Online (Sandbox Code Playgroud)
这本质上是检查是否可以delete window["alert"]["toString"]成功执行。如果我们在这里查看我的代码:
Object.defineProperty(window, 'alert', { configurable: false, writable: false });
Run Code Online (Sandbox Code Playgroud)
我自己冻结警报对象,因此此delete操作将失败,因为您无法toString从冻结对象中删除诸如 之类的属性或方法。
这是 Proxy 对象第一次发挥作用的地方。正如您可以从我的代码中读到的那样,我们实际上并没有设置window.alert = alert,而是正在执行window.alert = alertProxy。这个alertProxy基本上是const alert = () => { }我们之前定义的,但它还有一个额外的优点,我们可以为其指定某些处理程序:
const antiTamperCheckFaker = {
deleteProperty() {
return true;
},
getPrototypeOf() {
return () => { };
}
}
Run Code Online (Sandbox Code Playgroud)
为了绕过这个删除检查,我们有点“挂钩”删除函数,该函数在delete window["alert"]["toString"]执行时被调用。我们正在覆盖默认行为,以不删除任何内容,而是简单地说“一切都好,已删除!”。我们不能toString删除它,因为我们也覆盖了它,稍后我们需要它。
接下来是typeof Object.getPrototypeOf(check.ob[check.fun]) !== 'function'检查,我们可以将其写为:
typeof Object.getPrototypeOf(window["alert"]) !== 'function'
Run Code Online (Sandbox Code Playgroud)
这会检查 的原型是否window.alert是一个函数(*)。这不一定是问题,因为我们的“警报参与者”应该是一个函数,但我根据经验记得,在某些情况下,代理的使用可能会触发这样的检查。如果我们想要或必须为我们的警报参与者使用对象而不是函数,那么这个检查也会失败。如果需要的话,可以使用getPrototypeOf我们内部的处理程序来绕过这个问题。antiTamperCheckFaker
(*) 虽然它清楚地说 !== 'function',但如果任何检查为 true,则 if 子句作为整体返回 true,因此 ifwindow.alert不是函数,则检查的该部分将评估为 true,从而使检查返回true,这是不好的。
然后是check.ob[check.fun] !== 'function'检查,检查window.alert它本身是否是一个函数。非常不言自明,我们用来替换的警报window.alert只需是一个函数即可。
如果我们可以用一个对象替换的话,我们可以更容易地绕过这些检查window.alert,但是这个检查本质上是要求我们替换的任何东西都window.alert必须是某种函数。
然后我们有check.ob[check.fun].name !== check.fun检查。该函数检查当前函数的名称是否alert实际上window未命名为alert。如果我们有:
const myPatchedAlert = () => { }
代替:
const alert = () => { }
check.ob[check.fun].name在这种情况下,将评估为“myPatchedAlert” 。
然后我们有一张更大的支票:
check.ob[check.fun].toString().replaceAll('\n', '').match(regex) === null
Run Code Online (Sandbox Code Playgroud)
现在这个很有趣。如果您在开发控制台中输入内容,您可以亲眼看看会发生什么window.alert.toString(),但我会为您破坏它并说它将输出'function alert() { [native code] }'. 我们之前看到的丑陋的正则表达式,所以这个:
const regex = new RegExp('^function alert \\(\\)\\s*{\\s*\\[native code]\\s*}$');
Run Code Online (Sandbox Code Playgroud)
本质上只是进行比较,例如:
window.alert.toString() === 'function alert() { [native code] }'
当我们的警报const alert = () => { }被.toString()调用时,它将输出() => { }导致此检查失败的信息。幸运的是,我们重写了toString()警报的方法以返回类似的[本机代码]:
alert.toString = () => { return "function alert() { [native code] }" }
Run Code Online (Sandbox Code Playgroud)
虽然创建我们自己的toString()方法并让它返回我们想要的任何内容很容易,但请记住,早期的篡改检查实际上是toString()在运行此检查之前尝试从警报中删除该方法。如果无法删除它,检查就会失败。
我们toString()通过冻结自己的警报来保护我们的方法不被删除,但我们还必须拦截删除操作,以便检查会认为它已成功删除了不可删除的toString().
顺便说一句,我们不能使用Function.prototype.toString = ...,这会保护它不被删除,因为它将是一个原型函数而不是alert 的成员,因为检查也恰好检查 of 是否toString已Function.prototype被篡改。
事实上,事实并非如此。Bergi 给出了一个巧妙Function.prototype的解决方案:
Function.prototype.toString = function toString() {
return `function ${this.name}() { [native code] }`;
};
Run Code Online (Sandbox Code Playgroud)
我必须道歉。整件事有点失控了。SO 可能不是像我在这里那样进行自我实现冒险的最佳场所。
为了我的辩护,我确实为许多我想的关于扩展、脚本注入、防篡改方法和浏览器安全性的不寻常问题提出了不错的解决方案。希望也许有人发现自己在这里寻找答案。
{
"name": "document_start script exector",
"description": "document_start script exector",
"version": "1.0",
"manifest_version": 2,
"permissions": [
"declarativeNetRequest",
"declarativeNetRequestWithHostAccess",
"declarativeNetRequestFeedback",
"*://<website>/*"
],
"content_scripts": [
{
"matches": ["<website>"],
"js": ["inject.js"],
"run_at": "document_start"
}
],
"declarative_net_request" : {
"rule_resources" : [
{
"id": "ruleset_1",
"enabled": true,
"path": "rules.json"
}
]
}
}
Run Code Online (Sandbox Code Playgroud)
[
{
"id" : 1,
"priority": 1,
"action" : {
"type" : "modifyHeaders",
"responseHeaders": [
{ "header": "content-security-policy", "operation": "set", "value": "default-src * data: blob: filesystem: about: ws: wss: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; " }
]
},
"condition" : {
"urlFilter": "*://*/*",
"resourceTypes": ["main_frame", "sub_frame", "script", "other"]
}
}
]
Run Code Online (Sandbox Code Playgroud)
class ScriptInterceptor {
#observer
#config = { childList: true, subtree: true };
#handlerFn
constructor(handlerFn) {
this.#handlerFn = handlerFn;
this.#observer = new MutationObserver(this.#callback.bind(this));
this.#observer.observe(document, this.#config);
}
#isScriptMutation = (node) => {
return node.tagName === "SCRIPT";
}
#callback(mutationList, observer) {
for(const mutation of mutationList) {
for(const node of mutation.addedNodes) {
if(this.#isScriptMutation(node)) {
this.#handlerFn(node);
}
}
}
}
}
const scriptTrace = "Object.defineProperty(window,'alert',{configurable:false,writable:false})";
const interceptor = new ScriptInterceptor((script) => {
if(script.textContent.includes(scriptTrace)) {
script.textContent = "";
script.remove();
}
});
const inject = () => {
const fn = () => {
// Alert actor
const alert = () => { }
alert.toString = () => { return "function alert() { [native code] }" }
const antiTamperCheckFaker = {
deleteProperty() {
return true;
},
getPrototypeOf() {
return () => { };
}
}
const alertProxy = new Proxy(alert, antiTamperCheckFaker);
window.alert = alertProxy;
Object.defineProperty(window, 'alert', { configurable: false, writable: false });
}
const e = document.createElement('script');
e.text = `(${fn.toString()})()`;
document.documentElement.appendChild(e);
}
inject();
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
424 次 |
| 最近记录: |