XSS高阶案例--循环
一,一个循环
先看源码:首先是截取#后面的值,然后创建了一个div,再把#后面的值赋值给了div。
再在for循环中的querySelectorAll就相当于把div下的所有子元素全部选中,然后获取到了子元素的属性,
然后把这些属性全部删除。
因为有innerHTML,所以<script>alert(1)</script>传不进去
只能用属性值传<img src=1 onerror=alert(1)>
但它把src这个属性删了,留下了onerror这个属性
我们断点调试看一下
开始的el还没有值,我们往下走
现在把img的值传了进来
再往下,它通过attr.name就把src属性删除掉了
再往下走,就结束了,说明onerror属性就没有删除
这也说明了,这个代码是边循环边删除的(这是非常危险的)
既然它有删除有留下的,所以我们试试加其他属性看看
我在原先的每个属性前都加了一个属性,直接就成功了
二,两个循环
这里是加了一个数组,将选中的属性放进数组里,再在数组里面进行循环
这也是上一个循环的解决方案
显然用上一个的方法是不行了,所以我们考虑,就让它不要进到循环或者进循环删除无用的数据
我们先考虑进循环删除无用数据
假如有一个元素可以劫持el.attributes,那么就有可能删除的就不是el.attributes,就只是删除它里面的一个子元素
假如我们这样做,让它进去删除img,而让form触发
这里说el.attributes不是一个可迭代对象,其实我们已经进去了,但代码里是for循环,for循环一定要是可迭代对象,
因为我们只进去了一个img,那么就不可能是可迭代对象。
所以我们要把进去的组成一个集合或一个数组
input也是可以的,因为input里面有onfocus,所以我们用tabindex将元素聚焦
我们尝试一下,还是可行的,但是我们是无限聚焦在input上的,我们点确定一直都在那,
<form tabindex=1 onfocus="alert(1)" autofocus="ture"><input name=attributes><input name=attributes></form>
所以我们想让聚焦执行完把它删除掉,然后就成功了
<form tabindex=1 onfocus="alert(1);this.removeAttribute('onfocus');"autofocus="ture"><input name=attributes><input name=attributes></form>
不进循环
先写答案
它可以在过滤代码执行以前,提前执行恶意代码
DOM树的构建
我们知道JS是通过DOM接口来操作文档的,而HTML文档也是用DOM树来表示。所以在浏览器的渲染过程中,我们最关注的就是DOM树是如何构建的。
解析一份文档时,先由标记生成器做词法分析,将读入的字符转化为不同类型的Token,然后将Token传递给树构造器处理;接着标识识别器继续接收字符转换为Token,如此循环。实际上对于很多其他语言,词法分析全部完成后才会进行语法分析(树构造器完成的内容),但由于HTML的特殊性,树构造器工作的时候有可能会修改文档的内容,因此这个过程需要循环处理。
在树构建过程中,遇到不同的Token有不同的处理方式。具体的判断是在HTMLTreeBuilder::ProcessToken(AtomicHTMLToken* token)
中进行的。AtomicHTMLToken
是代表Token的数据结构,包含了确定Token类型的字段,确定Token名字的字段等等。Token类型共有7种,kStartTag
代表开标签,kEndTag
代表闭标签,kCharacter
代表标签内的文本。所以一个<script>alert(1)</script>
会被解析成3个不同种类的Token,分别是kStartTag
、kCharacter
和kEndTag
。在处理Token的过程中,还有一个InsertionMode
的概念,用于判断和辅助处理一些异常情况。
在处理Token的时候,还会用到HTMLElementStack
,一个栈的结构。当解析器遇到开标签时,会创建相应元素并附加到其父节点,然后将token和元素构成的Item压入该栈。遇到一个闭标签的时候,就会一直弹出栈直到遇到对应元素构成的item为止,这也是一个处理文档异常的办法。比如<div><p>1</div>
会被浏览器正确识别成<div><p>1</p></div>
正是借助了栈的能力。
而当处理script的闭标签时,除了弹出相应item,还会暂停当前的DOM树构建,进入JS的执行环境。换句话说,在文档中的script标签会阻塞DOM的构造。JS环境里对DOM操作又会导致回流,为DOM树构造造成额外影响。
img失败原因
先来找一下失败案例的原因,看看是在哪里触发了img payload中的事件代码。将过滤的代码注释以后,注入payload并打断点调试一下。
可以发现即使代码已经执行到最后一步,但在没有退出JS环境以前依然还没有弹窗。
此时再点击单步调试就会来到我们的代码的执行环境了。此外,这里还有一个细节就是appendChild
被注释并不影响代码的执行,证明即使img元素没有被添加到DOM树也不影响相关资源的加载和事件的触发。
那么很明显,alert(1)
是在页面上script标签中的代码全部执行完毕以后才被调用的。这里涉及到浏览器渲染的另外一部分内容: 在DOM树构建完成以后,就会触发DOMContentLoaded
事件,接着加载脚本、图片等外部文件,全部加载完成之后触发load
事件。
同时,上文已经提到了,页面的JS执行是会阻塞DOM树构建的。所以总的来说,在script标签内的JS执行完毕以后,DOM树才会构建完成,接着才会加载图片,然后发现加载内容出错才会触发error
事件。
可以在页面上添加以下代码来测试这一点。
window.addEventListener("DOMContentLoaded", (event) => {
console.log('DOMContentLoaded')
});
window.addEventListener("load", (event) => {
console.log('load')
});
测试结果
那么失败的原因也很明显了,由于js阻塞dom树,一直到js语句执行结束后,才可以引入img,此时img的属性已经被sanitizer清除了,自然也不可能执行事件代码了。
svg成功原因
我们可以通过断点调试来看一下
带着这个疑问,我们来看一下浏览器是怎么处理的。
上文提到了一个叫HTMLElementStack的结构用来帮助构建DOM树,它有多个出栈函数。其中,除了PopAll
以外,大部分出栈函数最终会调用到PopCommon
函数。这两个函数代码如下:
void HTMLElementStack::PopAll() { root_node_ = nullptr; head_element_ = nullptr; body_element_ = nullptr; stack_depth_ = 0; while (top_) { Node& node = *TopNode(); auto* element = DynamicTo<Element>(node); if (element) { element->FinishParsingChildren(); if (auto* select = DynamicTo<HTMLSelectElement>(node)) select->SetBlocksFormSubmission(true); } top_ = top_->ReleaseNext(); } } void HTMLElementStack::PopCommon() { DCHECK(!TopStackItem()->HasTagName(html_names::kHTMLTag)); DCHECK(!TopStackItem()->HasTagName(html_names::kHeadTag) || !head_element_); DCHECK(!TopStackItem()->HasTagName(html_names::kBodyTag) || !body_element_); Top()->FinishParsingChildren(); top_ = top_->ReleaseNext(); stack_depth_--; }
void SVGSVGElement::FinishParsingChildren() { SVGGraphicsElement::FinishParsingChildren(); // The outermost SVGSVGElement SVGLoad event is fired through // LocalDOMWindow::dispatchWindowLoadEvent. if (IsOutermostSVGSVGElement()) return; // finishParsingChildren() is called when the close tag is reached for an // element (e.g. </svg>) we send SVGLoad events here if we can, otherwise // they'll be sent when any required loads finish SendSVGLoadEventIfPossible(); }
bool SVGElement::SendSVGLoadEventIfPossible() { if (!HaveLoadedRequiredResources()) return false; if ((IsStructurallyExternal() || IsA<SVGSVGElement>(*this)) && HasLoadListener(this)) DispatchEvent(*Event::Create(event_type_names::kLoad)); return true; } 先决条件 在于svg不能最外层 onload 必须保证不是最外层
实验
我们可以将过滤的代码注释,并添加相关代码来验证这个事件的触发时间。
window.addEventListener("DOMContentLoaded", (event) => {
console.log('DOMContentLoaded')
});
window.addEventListener("load", (event) => {
console.log('load')
});
可以看到结果不出所料,最内层的svg先触发,然后再到下一层,而且是在DOM树构建完成以前就触发了相关事件;最外层的svg则得等到DOM树构建完成才能触发。
小结
img和其他payload的失败原因在于sanitizer执行的时间早于事件代码的执行时间,sanitizer将恶意代码清除了。
套嵌的svg之所以成功,是因为当页面为root.innerHtml
赋值的时候浏览器进入DOM树构建过程;在这个过程中会触发非最外层svg标签的load
事件,最终成功执行代码。所以,sanitizer执行的时间点在这之后,无法影响我们的payload。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!