Skip to content

Infostealers & Session Hijacking

💡
Before you start

Python 3 and a terminal. Nothing is downloaded, no real cookie store is opened, and nothing leaves your machine. macOS and Linux include Python; on Windows install it from python.org with “Add python.exe to PATH” ticked, then check with python3 --version.

Everything below is a model, deliberately. The “theft” is a file copy between two folders you create, the “attacker's machine” is a subdirectory, and no browser is involved. That is enough, because the mechanism being demonstrated is not a clever exploit — it is the ordinary way sessions work, which is exactly what makes it worth understanding.

The Attack That Ignores Your Password

You did everything correctly. You chose a long unique password, you turned on two-factor authentication, and you still lost the account. This is the pattern that confuses people most, and it has a specific explanation: the attacker never used your password at all.

An infostealer is a small piece of malware with one job. It runs once on your computer, copies everything of value it can reach in a few seconds, sends that package to the attacker, and frequently deletes itself. It is not ransomware and it does not want your attention -- the ideal outcome for the operator is that you never notice anything happened.

The scale is the part most people underestimate. Threat-intelligence firm Flashpoint reported roughly 1.8 billion credentials stolen during 2025 from approximately 5.8 million infected devices, alongside tens of billions of browser cookies. This is a commodity industry with subscription pricing, customer support, and marketplaces where the stolen bundles -- known as stealer logs -- are resold.

What Gets Taken in Those Few Seconds

  • Every password saved in your browser -- browser password stores are designed to be readable by the logged-in user, so malware running as you can read them too
  • Session cookies -- the single most valuable item, explained below
  • Autofill data -- names, addresses, phone numbers, and often stored card details
  • Cryptocurrency wallet files and extension data -- crypto wallets are a primary target because the theft is irreversible
  • Messaging application tokens -- including desktop chat clients, which gives access to your conversations and contacts
  • Files matching keyword patterns -- documents with names suggesting passwords, seed phrases, recovery codes or tax records
  • A system profile and screenshot -- used to price the log and to help the buyer impersonate your device convincingly

Why a Cookie Beats a Password

When you log in to a site and complete your second factor, the site issues your browser a session cookie. That cookie is the proof that you already authenticated. Every subsequent page load presents the cookie rather than making you log in again -- which is why you are not asked for your password on every click.

A stolen session cookie is therefore post-authentication material. An attacker who imports it into their own browser is not attempting to log in; from the site's point of view they are already logged in. There is no password prompt to intercept and no second factor to satisfy, because both of those steps already happened -- on your machine, by you.

⚠️
This is why "I had 2FA on" is not a contradiction.

Two-factor authentication protects the act of logging in. It does not protect a session that is already open. Changing your password later does not necessarily help either -- on many services, existing sessions survive a password change unless you explicitly revoke them.

The same theory explains a pattern seen repeatedly in ransomware cases: corporate credentials and session tokens appearing for sale before the intrusion, so the eventual attacker simply logs in rather than breaking in.

How Infection Actually Happens

Almost every consumer infection traces back to the victim running the program themselves. These are the routes that dominate.

  • Cracked software, key generators and game cheats -- the most reliable delivery channel there is, because the victim expects antivirus to complain and disables it in advance
  • Fake download pages promoted through search ads -- an advert for a legitimate tool leading to a lookalike site. Always scroll past the sponsored results to the real domain
  • "Fix this error by pasting this command" -- a page claims your browser needs a repair and asks you to paste a prepared command into a terminal or Run box. What you paste downloads and runs the stealer. No legitimate website ever needs this
  • Fake CAPTCHA and "verify you are human" prompts that ask for a keyboard sequence -- a variant of the same trick
  • Malicious browser extensions -- including previously honest extensions sold to a new owner who pushes a hostile update to existing users
  • Sponsorship and collaboration emails targeting creators, where the "brand brief" or "game build" is the payload
  • Attachments in archives with a password -- the password is in the email body specifically so the archive cannot be scanned in transit
💡
The rule that closes most of these at once.

Never paste a command you did not write into a terminal, and never run an installer that arrived through an advert, a direct message, or a search result you did not verify. Go to the official domain and download from there.

Reducing Your Exposure

  • Stop storing passwords in the browser -- use a dedicated password manager with its own encryption and a master password that is never saved in the browser. Browser stores are the first thing a stealer reads
  • Adopt passkeys wherever they are offered -- a passkey is bound to your device and cannot be copied out of it in a readable form, which removes it from the set of things a stealer can usefully take
  • Keep high-value accounts out of the everyday browser -- use a separate browser profile, or a separate device, for banking and crypto
  • Audit your extensions and remove what you do not use -- every extension is code with access to your pages, and ownership can change without notice
  • Do not run cracked software -- there is no safe way to do this, and the economics of the entire stealer industry depend on people continuing to try
  • Keep the operating system and browser updated and leave built-in protections such as SmartScreen or Safe Browsing enabled
  • Log out of sessions you are not using -- a session that does not exist cannot be stolen

Signs You May Already Be in a Stealer Log

  • A login alert from a country you have never visited, especially one that did not challenge for your second factor
  • Sessions or devices you do not recognise in an account's security settings
  • Messages sent from your accounts that you did not write
  • Password reset emails you never requested, arriving for several services at once
  • A previously installed extension behaving differently, or one you do not remember adding
  • Your address appearing in a stealer-log dataset on a reputable breach-notification service

Recovering Properly

Order matters here. Changing passwords first on an infected machine simply hands the attacker the new ones.

1
Clean or rebuild the device first.

Disconnect it from the network. A full reinstall of the operating system is the only fully trustworthy outcome. If you clean rather than rebuild, understand that you are accepting residual risk.

2
Revoke every active session, everywhere.

Look for "sign out of all devices", "revoke sessions" or "where you are logged in" in each account's security settings. This is the step that actually evicts an attacker holding your cookies, and it is the step people skip.

3
Change passwords from a known-clean device.

Start with email, then banking and crypto, then everything reused. Email comes first because it is the reset channel for everything else.

4
Rotate your second factors and recovery codes.

Regenerate backup codes, since the old set may have been in a stolen file, and re-enrol authenticator apps where the service allows it.

5
Move any cryptocurrency to a new wallet.

Generate a fresh seed phrase on a clean device. If a wallet file or seed phrase was on the infected machine, that wallet must be treated as permanently compromised.

Steal a Session and Then Stop It Working, in Five Steps

The confusing part of session theft is the aftermath. The victim's password still works, no “new sign-in” email ever arrived, two-factor authentication was switched on the whole time, and the account was nevertheless being used by somebody else. None of that is contradictory once you have seen what a session token is. In the next twenty minutes you will issue one, prove it is worth more than the password, copy it the way an infostealer does, replay it from “another machine”, and then add the three controls that make the copy useless. Every line of output below came from running these files.

1
Log in properly, with a password and a code

Go: open a terminal in a folder you can write to — cd ~/Desktop on macOS or Linux, cd %USERPROFILE%\Desktop on Windows.

Do: save this as login.py and run python3 login.py. This is an ordinary, correctly built login: the password is checked, then a second factor, and only then is a session created.

"""A perfectly ordinary login. Password, then a code from your phone."""
import json, secrets, time

USERS = {"sarah": {"password": "correct-horse-battery", "totp": "418907"}}
SESSIONS = "sessions.json"

def issue(user):
    token = secrets.token_hex(24)
    store = {token: {"user": user, "created": int(time.time())}}
    json.dump(store, open(SESSIONS, "w"))
    return token

def log_in(user, password, code):
    rec = USERS.get(user)
    if not rec or rec["password"] != password:
        return None, "wrong password"
    if rec["totp"] != code:
        return None, "wrong 2FA code"
    return issue(user), "logged in"

token, why = log_in("sarah", "correct-horse-battery", "418907")
print("password checked :", "yes")
print("2FA code checked :", "yes")
print("result           :", why)
print("cookie issued    :", len(token), "hex characters =", len(token) * 4, "bits")
print()
print("This token is now written to sessions.json, and in a real browser")
print("to the cookie store on disk. It is what proves you are Sarah.")

You should see: a successful login and a token written to disk:

password checked : yes
2FA code checked : yes
result           : logged in
cookie issued    : 48 hex characters = 192 bits

This token is now written to sessions.json, and in a real browser
to the cookie store on disk. It is what proves you are Sarah.

The token's value is different every run and is deliberately not printed — 192 bits of randomness is far beyond guessing, which is exactly right and is not the weakness here. Note what the server has just done: it verified two factors once, and then created something that stands in for that verification from now on. Every website works this way, because the alternative is asking for your password on every click.

If not: PermissionError means the folder is not writable — cd somewhere you own. If it prints wrong 2FA code, the string in USERS and the one passed to log_in differ; both are "418907".

2
Ask what the token alone can do

Go: the same folder.

Do: save this as peek.py and run python3 peek.py.

"""What the token alone is worth."""
import json

store = json.load(open("sessions.json"))
token = list(store)[0]

def whoami(presented_token):
    rec = store.get(presented_token)
    return rec["user"] if rec else None

print("the server is asked: who is holding this token?")
print("   answer:", whoami(token))
print()
print("password required to get that answer :", "no")
print("2FA code required                    :", "no")
print("device or location checked           :", "no")
print()
print("A session token is a bearer credential: whoever holds it IS the user,")
print("for as long as it lasts. That is the whole design, and it is why")
print("stealing one is worth more than stealing a password.")

You should see: full identification, with nothing else supplied:

the server is asked: who is holding this token?
   answer: sarah

password required to get that answer : no
2FA code required                    : no
device or location checked           : no

A session token is a bearer credential: whoever holds it IS the user,
for as long as it lasts. That is the whole design, and it is why
stealing one is worth more than stealing a password.

“Bearer credential” is the term worth keeping. It means possession is the whole of the claim — like a train ticket, which does not care who is holding it. A password is something you know and can be protected by a second factor; a session token is something you have, and once someone else also has it, they have the same thing you do.

This is why session theft outranks password theft for an attacker. A stolen password meets your 2FA. A stolen session was issued after your 2FA.

If not: FileNotFoundError: sessions.json means step 1 has not been run in this folder. If answer prints None, the token being looked up is not the one in the file — the script deliberately reads whichever token is there rather than hard-coding one.

3
Copy the file, the way the malware does

Go: the same folder.

Do: save this as exfiltrate.py and run python3 exfiltrate.py. Real infostealers sweep browser profile folders for the cookie database and the saved-password store; sessions.json stands in for those.

"""What an infostealer actually does: copy files, then leave."""
import json, os, shutil

# The real thing sweeps browser profile directories for exactly these.
WANTED = ["sessions.json"]          # stands for cookies.sqlite / Login Data / Local State

os.makedirs("attacker-machine", exist_ok=True)
taken = []
for name in WANTED:
    if os.path.exists(name):
        shutil.copy(name, os.path.join("attacker-machine", name))
        taken.append((name, os.path.getsize(name)))

for name, size in taken:
    print("copied %-16s %d bytes" % (name, size))
print()
print("time on the machine :", "under a second")
print("files changed       :", 0)
print("processes left behind:", 0)
print()
print("Nothing was encrypted, nothing was deleted, nothing popped up.")
print("There is no moment at which the victim notices anything.")

You should see: a copy taken, and nothing else touched:

copied sessions.json    94 bytes

time on the machine : under a second
files changed       : 0
processes left behind: 0

Nothing was encrypted, nothing was deleted, nothing popped up.
There is no moment at which the victim notices anything.

Read the three zeros again. This is the reason infostealer infections are usually discovered weeks later, from the consequences rather than the event: the software has no reason to stay, nothing to encrypt and no need for persistence. It runs once, copies files, and leaves.

It is also why the usual reassurance — “my computer is running fine, so I'm clearly not infected” — carries no information at all.

If not: if copied does not appear, sessions.json is missing; re-run step 1. The byte count is stable at 94 for this file, so a different number means the token length or the record shape was changed.

4
Use the stolen token from somewhere else entirely

Go: the same folder.

Do: save this as replay.py and run python3 replay.py.

"""The attacker, on his own computer, in another country."""
import json

# He never saw the password and never had the phone. He has the file.
stolen = json.load(open("attacker-machine/sessions.json"))
token = list(stolen)[0]

# The real server, checking the token it issued.
server = json.load(open("sessions.json"))

def whoami(presented_token):
    rec = server.get(presented_token)
    return rec["user"] if rec else None

print("attacker presents the stolen token")
print("   server says you are:", whoami(token))
print()
print("password entered :", "never")
print("2FA prompt shown :", "never")
print("login email sent :", "never -- no login happened")
print()
print("This is why victims report 'I was hacked but my password still works")
print("and I got no alert'. There was no login to alert on.")

You should see: the attacker recognised as the user, with no login taking place:

attacker presents the stolen token
   server says you are: sarah

password entered : never
2FA prompt shown : never
login email sent : never -- no login happened

This is why victims report 'I was hacked but my password still works
and I got no alert'. There was no login to alert on.

The last two lines explain the whole confusing aftermath. There was no login, so there is no login alert, no new-device email, and nothing in the “recent sign-ins” list to look at. Changing the password afterwards does not help either, unless the service also invalidates existing sessions — the token was issued before the change and, unless explicitly revoked, remains valid after it.

So if you ever suspect this has happened to you, the action is not only “change the password” but “sign out of all devices” — which most major services offer, usually under security settings, and which is the control that actually kills the stolen token.

If not: if the server does not recognise the token, the copy in attacker-machine/ is from an earlier run — delete that folder and repeat step 3, because each run of step 1 issues a new token.

5
Add the three controls that make the copy worthless

Go: the same folder.

Do: save this as bind.py and run python3 bind.py.

"""Three defences, tested against the same stolen token."""
import json, time

server = json.load(open("sessions.json"))
token = list(server)[0]
rec = server[token]

# The server records what the session was born on, not just who it belongs to.
rec["ip_prefix"] = "203.0.113"
rec["user_agent"] = "Firefox/141.0 Linux"
rec["created"] = int(time.time()) - 60 * 60 * 30      # thirty hours ago

MAX_AGE = 60 * 60 * 24                                 # sessions expire after a day

def check(presented, ip_prefix, user_agent, action="read"):
    r = server.get(presented)
    if not r:
        return "rejected: unknown token"
    if time.time() - r["created"] > MAX_AGE:
        return "rejected: session older than %d hours" % (MAX_AGE // 3600)
    if ip_prefix != r["ip_prefix"] or user_agent != r["user_agent"]:
        return "rejected: does not match the device this session was created on"
    if action == "change-email":
        return "allowed to read, but re-enter your password to change the email"
    return "accepted as " + r["user"]

print("legitimate user, same laptop, session 30 hours old")
print("  ", check(token, "203.0.113", "Firefox/141.0 Linux"))
print()
rec["created"] = int(time.time())
print("legitimate user, same laptop, fresh session")
print("  ", check(token, "203.0.113", "Firefox/141.0 Linux"))
print()
print("attacker, stolen token, his own machine")
print("  ", check(token, "185.244.31", "Chrome/139.0 Windows"))
print()
print("attacker, stolen token, trying to change the recovery email")
print("  ", check(token, "203.0.113", "Firefox/141.0 Linux", action="change-email"))

You should see: the same stolen token failing, and a legitimate session surviving:

legitimate user, same laptop, session 30 hours old
   rejected: session older than 24 hours

legitimate user, same laptop, fresh session
   accepted as sarah

attacker, stolen token, his own machine
   rejected: does not match the device this session was created on

attacker, stolen token, trying to change the recovery email
   allowed to read, but re-enter your password to change the email

Three separate controls are at work, and the fourth case is the subtlest. The expiry limits how long a theft is useful. The binding ties the session to the device and network it was created on, so a token used from elsewhere is refused. And step-up authentication means that even a session which passes both checks must re-enter the password before changing the recovery email — because that single change is how a temporary intrusion becomes permanent ownership of the account.

None of these is something you configure as a user; they are decisions the service made. What you can do is prefer services that make them, and use the two controls you do have: review active sessions occasionally, and sign out of all devices whenever anything feels wrong.

If not: if the first case is accepted rather than rejected, the arithmetic on the created line was altered — it must place the session thirty hours in the past, which is beyond the one-day MAX_AGE. If every case is rejected as unknown token, re-run step 1 first.

🎉
Check yourself before moving on

Without scrolling up: someone tells you their account was accessed even though they have two-factor authentication on and their password was never used anywhere else. They have changed the password and want to know if they are safe now. What happened, and what would you tell them to do? Answer: almost certainly a session token was copied from their machine rather than a password being cracked — the token was issued after their two-factor check, so it carries that verification with it and prompts for nothing. That is why there was no sign-in alert: no sign-in occurred. Changing the password is not sufficient on its own, because an already-issued session usually survives a password change unless the service explicitly revokes it. What to tell them: use “sign out of all devices” on every important account, then change the password, then check the recovery email and phone number on the account, since altering those is how a stolen session is converted into permanent access. And find out how the token was taken — a cracked installer, a browser extension, or a file from a download site — because until that is dealt with the next session will be taken too.

Now do it without the page: extend bind.py with a fourth control: refuse any session that has not been used for more than an hour, even if it is younger than MAX_AGE. Then answer the part no code covers — open the security settings of the account you would least like to lose and find its list of active sessions. How many devices are listed? Can you name every one? Ending the ones you cannot is free, takes a minute, and is the exact defence step 4 described.

Summary

  • Infostealers take everything in seconds and leave quietly -- their goal is to go unnoticed
  • Session cookies are the crown jewels -- they represent an already-authenticated session, so 2FA is never challenged
  • Most infections are self-installed through cracked software, fake download ads, and pasted "fix it" commands
  • Browser password stores are a liability -- a dedicated manager and passkeys are meaningfully better
  • Recovery order matters -- clean the device, revoke sessions, then change passwords from somewhere trustworthy
  • Revoking sessions is the step that ends the intrusion -- a password change on its own may not
🎉
Two habits carry most of the benefit.

Install software only from its official source, and never paste a command you did not write. Those two rules eliminate the delivery routes behind the overwhelming majority of consumer infostealer infections.