• 复现xss绕过一个循环和两个循环


    目录

    原文:

    方法:

    两个循环:

    原文:

    方法一:

    浏览器的渲染过程:

    触发流程:

    方法二:(%20open%20οntοggle=alert(1)>

    details标签

    有时可行,有时不行。有延迟的话 肯定执行成功,因为此时异步事件已经执行完成,执行点在innerhtml。如果没有延迟,有可能在js删除属性之后,异步事件才执行完成。

     

    事件触发流程:

    结论


    原文:

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    7. <title>Documenttitle>
    8. head>
    9. <body>
    10. body>
    11. <script>
    12. const data = decodeURIComponent(location.hash.substr(1));
    13. const root = document.createElement('div');
    14. root.innerHTML = data;
    15. // 这里模拟了XSS过滤的过程,方法是移除所有属性
    16. for (let el of root.querySelectorAll('*')) {
    17. for (let attr of el.attributes) {
    18. el.removeAttribute(attr.name);
    19. }
    20. }
    21. document.body.appendChild(root);
    22. script>
    23. html>

    方法:

    基础原理:

    1. list = [1, 2, 3, 4, 5, 6]
    2. for i in list:
    3. if i == 2:
    4. list.remove(i)
    5. print(i)
    6. print(list)

    与这个结果类似

     

     

    两个循环:

    原文:

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    7. <title>Documenttitle>
    8. head>
    9. <body>
    10. <script>
    11. const data = decodeURIComponent(location.hash.substr(1));
    12. const root = document.createElement('div');
    13. root.innerHTML = data;
    14. // let details = root.querySelector("details")
    15. // root.removeChild(details)
    16. // 这里模拟了XSS过滤的过程,方法是移除所有属性
    17. for (let el of root.querySelectorAll('*')) {
    18. let attrs = [];
    19. for (let attr of el.attributes) {
    20. attrs.push(attr.name);
    21. }
    22. for (let name of attrs) {
    23. el.removeAttribute(name);
    24. }
    25. }
    26. document.body.appendChild(root);
    27. script>
    28. html>

    方法一:

    <svg>

     原理:

    看起来平平无奇,但是它可以在过滤代码执行以前,提前执行恶意代码。

    为更好地理解这个问题,需要稍微了解一下浏览器的渲染过程

    浏览器的渲染过程:

    HTML文档也是用DOM树来表示。所以在浏览器的渲染过程中,我们最关注的就是DOM树是如何构建的。

    解析一份文档时,先由标记生成器做词法分析,将读入的字符转化为不同类型的Token,然后将Token传递给树构造器处理;接着标识识别器继续接收字符转换为Token,如此循环。实际上对于很多其他语言,词法分析全部完成后才会进行语法分析(树构造器完成的内容),但由于HTML的特殊性,树构造器工作的时候有可能会修改文档的内容,因此这个过程需要循环处理

     

    在树构建过程中,遇到不同的Token有不同的处理方式。具体的判断是在HTMLTreeBuilder::ProcessToken(AtomicHTMLToken* token)中进行的。AtomicHTMLToken是代表Token的数据结构,包含了确定Token类型的字段,确定Token名字的字段等等。Token类型共有7种,kStartTag代表开标签,kEndTag代表闭标签,kCharacter代表标签内的文本。所以一个会被解析成3个不同种类的Token,分别是kStartTagkCharacterkEndTag。在处理Token的过程中。

    在处理Token的时候,还会用到HTMLElementStack,一个栈的结构。当解析器遇到开标签时,会创建相应元素并附加到其父节点,然后将token和元素构成的Item压入该栈。遇到一个闭标签的时候,就会一直弹出栈直到遇到对应元素构成的item为止,这也是一个处理文档异常的办法。比如

    1

    会被浏览器正确识别成<div>

    1

    正是借助了栈的能力。

    而当处理script的闭标签时,除了弹出相应item,还会暂停当前的DOM树构建,进入JS的执行环境。换句话说,在文档中的script标签会阻塞DOM的构造。JS环境里对DOM操作又会导致回流,为DOM树构造造成额外影响。

    总的来说,在script标签内的JS执行完毕以后,DOM树才会构建完成,接着才会加载图片,然后发现加载内容出错才会触发error事件

    继续用断点调试svg payload为何成功。

    在root.innerHTML = data;断下来后,点击单步调试。

     

     神奇的事情发生了,直接弹出了窗口,点击确定以后,调试器才会走到下一行代码。

    触发流程:

    上文提到了一个叫HTMLElementStack的结构用来帮助构建DOM树,它有多个出栈函数。其中,除了PopAll以外,大部分出栈函数最终会调用到PopCommon函数。这两个函数代码如下:

    1. void HTMLElementStack::PopAll() {
    2. root_node_ = nullptr;
    3. head_element_ = nullptr;
    4. body_element_ = nullptr;
    5. stack_depth_ = 0;
    6. while (top_) {
    7. Node& node = *TopNode();
    8. auto* element = DynamicTo(node);
    9. if (element) {
    10. element->FinishParsingChildren();
    11. if (auto* select = DynamicTo(node))
    12. select->SetBlocksFormSubmission(true);
    13. }
    14. top_ = top_->ReleaseNext();
    15. }
    16. }
    17. void HTMLElementStack::PopCommon() {
    18. DCHECK(!TopStackItem()->HasTagName(html_names::kHTMLTag));
    19. DCHECK(!TopStackItem()->HasTagName(html_names::kHeadTag) || !head_element_);
    20. DCHECK(!TopStackItem()->HasTagName(html_names::kBodyTag) || !body_element_);
    21. Top()->FinishParsingChildren();
    22. top_ = top_->ReleaseNext();
    23. stack_depth_--;
    24. }

    当我们没有正确闭合标签的时候,如,就可能调用到PopAll来清理;而正确闭合的标签就可能调用到其他出栈函数并调用到PopCommon。这两个函数有一个共同点,都会调用栈中元素的FinishParsingChildren函数。这个函数用于处理子节点解析完毕以后的工作。因此,我们可以查看svg标签对应的元素类的这个函数。

    1. void SVGSVGElement::FinishParsingChildren() {
    2. SVGGraphicsElement::FinishParsingChildren();
    3. // The outermost SVGSVGElement SVGLoad event is fired through
    4. // LocalDOMWindow::dispatchWindowLoadEvent.
    5. if (IsOutermostSVGSVGElement())
    6. return;
    7. // finishParsingChildren() is called when the close tag is reached for an
    8. // element (e.g. ) we send SVGLoad events here if we can, otherwise
    9. // they'll be sent when any required loads finish
    10. SendSVGLoadEventIfPossible();
    11. }

    这里有一个非常明显的判断IsOutermostSVGSVGElement,如果是最外层的svg则直接返回。注释也告诉我们了,最外层svg的load事件由LocalDOMWindow::dispatchWindowLoadEvent触发;而其他svg的load事件则在达到结束标记的时候触发。所以我们跟进SendSVGLoadEventIfPossible进一步查看。

    1. bool SVGElement::SendSVGLoadEventIfPossible() {
    2. if (!HaveLoadedRequiredResources())
    3. return false;
    4. if ((IsStructurallyExternal() || IsA(*this)) &&
    5. HasLoadListener(this))
    6. DispatchEvent(*Event::Create(event_type_names::kLoad));
    7. return true;
    8. }
    9. 先决条件 在于svg不能最外层 onload 必须保证不是最外层

    这个函数是继承自父类SVGElement的,可以看到代码中的DispatchEvent(*Event::Create(event_type_names::kLoad));确实触发了load事件,而前面的判断只要满足是svg元素以及对load事件编写了相关代码即可,也就是说在这里执行了我们写的onload=alert(1)的代码。

    小结:

    套嵌的svg之所以成功,是因为当页面为root.innerHtml赋值的时候浏览器进入DOM树构建过程;在这个过程中会触发非最外层svg标签的load事件,最终成功执行代码。所以,sanitizer执行的时间点在这之后,无法影响我们的payload。

    当然这种方法也可以在上个只有一个循环的xss中执行:

     

    方法二:

    details标签

    有时可行,有时不行。有延迟的话 肯定执行成功,因为此时异步事件已经执行完成,执行点在innerhtml。如果没有延迟,有可能在js删除属性之后,异步事件才执行完成。

    details 异步执行是将相应执行函数放进一个事件队列中,只要事件不停止,在你放入事件之后,你将details标签删除,已经没用了,因为事件会接着执行

    此时我们需要将上面原文中的两行注释打开:

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <meta http-equiv="X-UA-Compatible" content="IE=edge">
    6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    7. <title>Documenttitle>
    8. head>
    9. <body>
    10. <script>
    11. const data = decodeURIComponent(location.hash.substr(1));
    12. const root = document.createElement('div');
    13. root.innerHTML = data;
    14. let details = root.querySelector("details")
    15. root.removeChild(details)
    16. // 这里模拟了XSS过滤的过程,方法是移除所有属性
    17. for (let el of root.querySelectorAll('*')) {
    18. let attrs = [];
    19. for (let attr of el.attributes) {
    20. attrs.push(attr.name);
    21. }
    22. for (let name of attrs) {
    23. el.removeAttribute(name);
    24. }
    25. }
    26. document.body.appendChild(root);
    27. script>
    28. html>

    执行:

     

    事件触发流程:

    首先触发代码的点是在DispatchPendingEvent函数里

    1. void HTMLDetailsElement::DispatchPendingEvent(
    2. const AttributeModificationReason reason) {
    3. if (reason == AttributeModificationReason::kByParser)
    4. GetDocument().SetToggleDuringParsing(true);
    5. DispatchEvent(*Event::Create(event_type_names::kToggle));
    6. if (reason == AttributeModificationReason::kByParser)
    7. GetDocument().SetToggleDuringParsing(false);
    8. }

    而这个函数是在ParseAttribute被调用的

    1. void HTMLDetailsElement::ParseAttribute(
    2. const AttributeModificationParams& params) {
    3. if (params.name == html_names::kOpenAttr) {
    4. bool old_value = is_open_;
    5. is_open_ = !params.new_value.IsNull();
    6. if (is_open_ == old_value)
    7. return;
    8. // Dispatch toggle event asynchronously.
    9. pending_event_ = PostCancellableTask(
    10. *GetDocument().GetTaskRunner(TaskType::kDOMManipulation), FROM_HERE,
    11. WTF::Bind(&HTMLDetailsElement::DispatchPendingEvent,
    12. WrapPersistent(this), params.reason));
    13. ....
    14. return;
    15. }
    16. HTMLElement::ParseAttribute(params);
    17. }

    ParseAttribute正是在解析文档处理标签属性的时候被调用的。注释也写到了,分发toggle事件的操作是异步的。可以看到下面的代码是通过PostCancellableTask来进行回调触发的,并且传递了一个TaskRunner

    1. TaskHandle PostCancellableTask(base::SequencedTaskRunner& task_runner,
    2. const base::Location& location,
    3. base::OnceClosure task) {
    4. DCHECK(task_runner.RunsTasksInCurrentSequence());
    5. scoped_refptr runner =
    6. base::AdoptRef(new TaskHandle::Runner(std::move(task)));
    7. task_runner.PostTask(location,
    8. WTF::Bind(&TaskHandle::Runner::Run, runner->AsWeakPtr(),
    9. TaskHandle(runner)));
    10. return TaskHandle(runner);
    11. }

    跟进PostCancellableTask的代码则会发现,回调函数(被封装成task)正是通过传递的TaskRunner去派遣执行。

    清楚调用流程以后,就可以思考,为什么无法触发这个事件呢?最大的可能性,就是在任务交给TaskRunner以后又被取消了。因为是异步调用,而且PostCancellableTask这个函数名也暗示了这一点。

    结论

    details标签的toggle事件是异步触发的,并且直接对details标签的移除不会清除原先通过属性设置的异步任务

  • 相关阅读:
    PLC易学但是后期如何发展?
    知识问答产品利器:文本分段器实现自动知识加工
    网络间串扰降噪推荐掌握的经验法则
    精灵图和 base64 之间如何选择?
    物联网开发笔记(45)- 使用Micropython开发ESP32开发板之控制红外传感器
    证书显示未受信任,生成的证书过期
    Linux命令大全
    Python 程序的输出 | 第十一套(异常处理)
    Cadence orcad 原理图导出带书签目录的办法
    JAVA毕业设计科技项目在线评审系统计算机源码+lw文档+系统+调试部署+数据库
  • 原文地址:https://blog.csdn.net/weixin_59280309/article/details/126098775