Background Concepts

Expand any topic below for the same background material reused across the lab writeups. These are the building blocks that sit behind JWT, OAuth, SSRF, smuggling, PHP deserialization, and prototype pollution labs.

Fundamentals

HTTP Request Smuggling Basics click to expand

What is HTTP Request Smuggling?

HTTP request smuggling tricks a site into mixing up where one HTTP request ends and the next one starts.

Most apps sit behind a front-end (proxy / load balancer) that forwards traffic to a back-end. For speed, the front-end often sends several HTTP/1 requests over the same connection. That only works if both servers agree on where each request stops.

Why HTTP/1 can be unclear:

HTTP/1 has two ways to say how long the body is:

  1. Content-Length: exact size in bytes
  2. Transfer-Encoding: chunked: body sent in pieces, ending with a zero-size chunk (0\r\n\r\n)

If both headers are present and disagree, one server may use Content-Length and the other Transfer-Encoding. The attacker can then hide extra bytes that the back-end treats as the start of the next request.

Classic variants:

VariantFront-end usesBack-end uses
CL.TEContent-LengthTransfer-Encoding
TE.CLTransfer-EncodingContent-Length
TE.TEBoth support TE, but one can be tricked with a weird TE header

Impact can include:

  • Bypassing front-end security controls (rules on the proxy never see the hidden inner request)
  • Poisoning other users’ requests / responses (leftover bytes get stuck onto someone else’s next request)
  • Stealing credentials, tokens, or poisoning caches (capture or rewrite what another user sends or receives)
  • Reaching privileged endpoints (e.g. hide GET /internal so the back-end sees it while the front-end only allowed /, skipping path rewrites or auth headers the front-end would have added)
JWT Fundamentals click to expand

What is JWT (JSON Web Token)?

JWT is a compact, URL-safe means of representing Claims* to be transferred between two parties. It’s commonly used for authentication and information exchange in web applications. It uses base64 encoding to ensure URL-safe transmission.

A JWT consists of three parts separated by dots (.):

header.payload.signature

For example:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ3aWVuZXIiLCJleHAiOjE3NzIxOTg5NTd9.signature_here

Breaking down each part:

1. Header (Base64URL encoded JSON):

{
  "kid": "170c351e-58cc-4e4b-9116-6e7924337580",
  "alg": "HS256"
}
  • alg: The signing algorithm (HS256 = HMAC-SHA256)
  • kid: Key ID (optional identifier for the signing key)

2. Payload (Base64URL encoded JSON):

{
  "iss": "portswigger",
  "exp": 1772198957,
  "sub": "wiener"
}
  • iss: Issuer of the token
  • exp: Expiration timestamp
  • sub: Subject (usually the username)

3. Signature: The signature is created by taking the encoded header and payload, and signing them with a secret key using the algorithm specified in the header:

# Pseudocode

message = base64UrlEncode(header) + "." + base64UrlEncode(payload) 

key = JWT_SECRET # It could be a password, API key, UUID, or any other string

signature = HMACSHA256(message, key)  # This is just the signature part, not the complete JWT

The signature ensures that the token hasn’t been tampered with. If an attacker modifies the header or payload, the signature won’t match unless they know the secret key.

The complete JWT would be constructed as:

# Pseudocode

jwt = base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + signature
Understanding OAuth 2.0 & Authorization Flows click to expand

What is OAuth 2.0?

OAuth 2.0 is an authorization framework that allows third-party applications to access a user’s resources without exposing their credentials. Instead of sharing passwords, OAuth uses authorization codes and access tokens to grant limited access.

Think of OAuth like giving someone a temporary valet key for your car:

  • Without OAuth: You give someone your main car key (password). They can drive your car, open the trunk, access the glove box. Basically everything. If they lose it or steal it, they have full access to your car.

  • With OAuth: You give them a special valet key (access token) that only opens the driver’s door and starts the engine. They can’t access the trunk or glove box. The key expires after a short time, and only works for specific cars (limited permissions).

The key players in an OAuth flow are:

  • Resource Owner: The user who owns the data (e.g., the admin)
  • Client Application: The website that wants to access the user’s data (e.g., the blog)
  • OAuth Provider (Authorization Server): The service that authenticates the user and issues tokens (e.g., the social media login server)

Real-world example: When you click “Log in with Google” on a website, that website is the Client Application, Google is the OAuth Provider, and you are the Resource Owner. The website never sees your Google password. It just gets a temporary token to access basic profile information.

The Authorization Code Flow

The most common OAuth flow works like this:

Step-by-step breakdown:

  1. User clicks “Log in with Social Media” on the client app (e.g., clicking “Log in with Google” on a blog)

  2. Client app redirects user to the OAuth provider’s /auth endpoint with several parameters:

    • client_id: identifies the client application (like “this is the blog website”)
    • redirect_uri: where to send the user back after authorization (like “send them back to https://.com/oauth-callback”)
    • response_type=code: requests an authorization code (not direct access)
    • scope: what permissions are requested (like “I just need your email and profile picture”)
  3. User authenticates with the OAuth provider (enters their Google/Facebook credentials)

  4. User consents to the requested permissions (sees a screen like “Allow this blog to access your email and profile?”)

  5. OAuth provider redirects user back to the redirect_uri with an authorization code:

    https://<blog>.com/oauth-callback?code=XYZ123ABC

    This is like getting a receipt that says “the user approved this request”

  6. Client app exchanges the code for an access token (server-side, the blog sends the code back to Google saying “here’s the receipt, now give me the actual key”)

  7. Client app uses the access token to access user resources (the blog can now fetch the user’s profile info from Google using the temporary key)

Why this two-step process? The authorization code is short-lived and single-use. Even if an attacker intercepts it, they can only use it once and it expires quickly. The access token is what actually grants access, but it’s exchanged server-to-server, never exposed to the user’s browser.

HTTP Request Smuggling

CL.TE & Differential Responses click to expand

CL.TE Smuggling & Differential Responses

In a CL.TE vulnerability the front-end trusts Content-Length while the back-end trusts Transfer-Encoding: chunked.

A minimal smuggling request looks like:

POST / HTTP/1.1
Host: vulnerable-website.com
Content-Length: 13
Transfer-Encoding: chunked

0

SMUGGLED

What each server sees:

  1. Front-end (CL): body is 13 bytes, so it forwards everything through SMUGGLED
  2. Back-end (TE): sees chunk size 0, so the request ends immediately; leftover bytes SMUGGLED stay in the socket buffer
  3. Those leftover bytes are prepended to whatever request arrives next on that back-end connection

Confirming with differential responses

Timing delays can hint at smuggling, but a stronger proof is to force a different response than a normal request would get.

The usual pattern:

  1. Send an attack request that smuggles GET /404 HTTP/1.1... into the back-end buffer
  2. Immediately send a second request (ideally on a different client connection, same URL so load balancing still hits the same back-end)
  3. If the second response is 404 Not Found instead of the normal 200 OK for /, the smuggled prefix was applied and CL.TE is confirmed

After a successful smuggle, the back-end effectively processes something like:

GET /404 HTTP/1.1
X-Ignore: XPOST / HTTP/1.1
Host: ...
...

X-Ignore (or any dummy header) absorbs the start of the following real request so header parsing does not blow up before the path /404 is evaluated.

JWT

HS256 & Brute-force Attacks click to expand

What is HS256 (HMAC-SHA256)?

HS256 is a symmetric signing algorithm, meaning the same secret key is used to both sign and verify the token. This is different from asymmetric algorithms like RS256, which use a private key to sign and a public key to verify.

The security of HS256 depends entirely on the secrecy and strength of the key. If the key is weak (like “secret”, “password”, or “secret1”), it can be brute-forced.

Why is Brute-forcing Possible?

Since the signature is deterministic (same input + same key = same signature), an attacker can:

  1. Take the JWT’s header and payload
  2. Try signing it with different secret keys from a wordlist
  3. Compare the generated signature with the original
  4. When they match, the secret key has been found

This is only feasible when the secret is weak and exists in common wordlists.

JWT Algorithm Confusion Attack click to expand

What is a JWT Algorithm Confusion Attack?

JWT algorithm confusion (also known as key confusion) exploits servers that use asymmetric algorithms like RS256 but fail to enforce the expected algorithm when verifying tokens.

How RS256 normally works:

  • The server signs tokens with a private key
  • The server verifies tokens with the corresponding public key
  • The public key is often exposed via a standard endpoint like /jwks.json

The vulnerability:

When a server receives a JWT, it reads the alg header to determine which algorithm to use for verification. If the server doesn’t strictly enforce the expected algorithm, an attacker can:

  1. Obtain the server’s public key (often available at /jwks.json or /.well-known/jwks.json)
  2. Change the alg header from RS256 to HS256
  3. Sign the token using HMAC-SHA256 with the public key as the symmetric secret

The server then sees alg: HS256, picks its “verification key” (which is the RSA public key), and uses it as the HMAC secret exactly matching the attacker’s signature.

Normal flow: RS256 -> verify with public key (asymmetric)
Attack flow: HS256 -> verify with public key (used as symmetric secret)

This works because the server’s verification code generically passes its “key” to whatever algorithm is specified, without checking that the algorithm matches the key type.

JWT kid Header & Path Traversal click to expand

What is the kid Header Parameter?

The kid (Key ID) is an optional header parameter in a JWT that tells the server which key to use to verify the token’s signature. This is useful when a server manages multiple signing keys.

{
  "kid": "dade186d-6d19-4584-ab27-737975e1611f",
  "alg": "HS256"
}

The server receives the JWT, reads the kid value, and uses it to look up the corresponding signing key. The implementation of this lookup varies. Some servers use a database, others use the filesystem.

Path Traversal via kid

When the server fetches the signing key from the filesystem using the kid value, it becomes vulnerable to directory traversal if the input is not sanitized. An attacker can manipulate kid to point to any file with known contents:

{
  "kid": "../../../../../../../dev/null",
  "alg": "HS256"
}

On Linux, /dev/null is a special file that always returns empty content. By pointing kid to it, the server will use an empty string as the signing key. The attacker can then sign their forged JWT with an empty string, and the server will accept it as valid.

JWT Unverified Signature Vulnerability click to expand

JWT Unverified Signature Vulnerability

When a server receives a JWT, it should always verify the signature before trusting the token’s contents. However, some implementations fail to do this properly.

The JWT specification is flexible. It defines how tokens should be structured and signed, but the actual signature verification is left to the application’s code. This creates a dangerous gap: if the server only decodes the JWT without verifying the signature, an attacker can modify the payload at will.

How the attack works:

1. Attacker logs in and receives a valid JWT
2. Attacker decodes the JWT payload (base64)
3. Attacker modifies the payload (e.g., changes "sub": "wiener" to "sub": "administrator")
4. Attacker re-encodes the payload (base64)
5. Attacker sends the modified JWT to the server, which accepts it without checking the signature

The token’s header and signature can remain completely untouched only the payload needs to be modified.

Alternative attack: Changing algorithm to “none”

Some JWT implementations also accept the "none" algorithm, which completely disables signature verification. Attackers can:

1. Change the header's "alg" from "RS256" or "HS256" to "none"
2. Remove the signature part entirely (or keep any value)
3. The server accepts the token without any signature validation

This creates a token like: header.payload. (no signature) or header.payload.invalid_signature.

Why does this happen?

  • Libraries often provide separate methods for decoding and verifying JWTs
  • A developer might accidentally use decode() instead of verify(), which skips signature validation
  • The application trusts the token blindly once it can parse the JSON structure
  • Some libraries accept the "none" algorithm for unsigned tokens, intended for specific use cases but dangerous if not properly restricted

Misc

Backup Files & Source Code Leaks click to expand

What is a Backup File (~)?

Many text editors (like Vim, Emacs, and others) automatically create backup copies of files by appending a tilde (~) to the filename. For example, editing CustomTemplate.php creates CustomTemplate.php~. These backup files often get accidentally deployed to production servers and can leak source code to anyone who requests them, depending on the configuration of the web server it may serve them as plain text.

OAuth

OAuth redirect_uri Validation click to expand

Why is redirect_uri Validation Critical?

The redirect_uri parameter specifies where the OAuth provider should send the authorization code after user authentication. Without strict validation, an attacker can modify this parameter to point to a server they control. When the victim authenticates, their authorization code is intercepted by the attacker instead of being delivered to the legitimate application.

In OAuth terms:

  • Secure: The OAuth provider only redirects to pre-registered URLs (like https://<blog>.com/oauth-callback)
  • Vulnerable: The OAuth provider redirects to whatever URL the attacker provides (like https://<attacker-site>.com/steal-code)

Why is this so dangerous? Authorization codes are bearer tokens. Whoever has the code can exchange it for access to the victim’s account. It’s like finding someone’s car keys. If you have them, you can drive the car.

Object Injection

Understanding PHP Serialization click to expand

What is Serialization?

Serialization is the process of converting an object (a data structure in memory) into a format that can be stored or transmitted, like a string. Deserialization is the reverse: turning that string back into an object. In PHP, this is done with serialize() and unserialize().

For example, a PHP object like:

$user = new User();
$user->username = "wiener";
$user->access_token = "abc123";

Gets serialized into:

O:4:"User":2:{s:8:"username";s:6:"wiener";s:12:"access_token";s:6:"abc123";}

Breaking down the format:

  • O:4:"User"Object of class name length 4, named “User”
  • :2: — has 2 properties
  • s:8:"username"string property name of length 8: “username”
  • s:6:"wiener"string value of length 6: “wiener”

When an application stores serialized objects in cookies (like session tokens), an attacker can tamper with them. If the server blindly deserializes user-controlled input, it will reconstruct whatever object the attacker provides.

PHP

PHP Magic Methods click to expand

What are PHP Magic Methods?

PHP has special methods called magic methods that are automatically invoked at certain points in an object’s lifecycle. They always start with a double underscore (__). The most relevant ones for deserialization attacks are:

  • __construct() — called when an object is created (new ClassName())
  • __destruct() — called when an object is destroyed (goes out of scope, script ends, or garbage collected)
  • __wakeup() — called when an object is deserialized (unserialize())
  • __toString() — called when an object is treated as a string

The dangerous one here is __destruct(). When PHP deserializes an object from a cookie, it creates that object in memory. When the request finishes processing, PHP’s garbage collector destroys the object, which automatically triggers __destruct(). The attacker doesn’t need to call it — PHP does it for them.

Prototype Pollution

Javascript Prototype click to expand

Most JavaScript objects have a link to a special object called its prototype

That link is not a copy of properties. It is a fallback: when you read a key that the object does not own, the engine looks for it on the prototype.

Each prototype can link to another one in turn. Those links form a prototype chain, which ends at null.

So a missing property is searched in this order:

  1. the object itself (own properties)
  2. its prototype
  3. that prototype’s prototype, and so on, until either finds it or finds null and returns the famous undefined

Prototype chain behavior examples

A lookup can walk more than one link. Here user inherits from roleDefaults, which still inherits from Object.prototype:

const roleDefaults = {
  isAdmin: false
}

const user = {
  username: "carlos",
  __proto__: roleDefaults
}

console.log(user.username) // "carlos" (own)
console.log(user.isAdmin)  // false    (from roleDefaults)
console.log(user.toString) // [Function: toString] (from Object.prototype but was searched first in user and roleDefaults)

// The full chain:
// user -> roleDefaults -> Object.prototype -> null

A few more short examples

Undefined property default behavior:

const user = {
  username: "carlos"
}

console.log(user.isAdmin) // undefined

Undefined property but present on the prototype chain:

const user = {
  username: "carlos",
  __proto__: {
    isAdmin: true
  }
}

console.log(user.isAdmin) // true

Priority of properties:
Own properties always win over inherited ones. If the object already has the key, the chain is not consulted

const user = {
  username: "carlos",
  isAdmin: false,
  __proto__: {
    isAdmin: true
  }
}

console.log(user.isAdmin) // false
Node.js execArgv Gadget click to expand

Node.js execArgv as a Gadget

When you start Node yourself, you can pass CLI flags before the script:

node --inspect app.js
node --require ./preload.js app.js
node --eval "console.log(1)"

Those flags change how the Node process boots: enable the debugger, preload a module, or run inline JavaScript with --eval / -e.

child_process.fork() and spawn() can pass the same kind of flags to a child Node process through an options field named execArgv. It is just an array of strings, for example ["--inspect"] or ["--eval=console.log(1)"].

const { fork } = require("child_process")

fork("./worker.js", [], {
  execArgv: ["--eval=console.log('child boot')"]
})

If application code builds an options object and never sets its own execArgv, a normal property read still walks the prototype chain:

const options = {}

options.execArgv // undefined

Object.prototype.execArgv = ["--eval=console.log('polluted')"]

options.execArgv // ["--eval=console.log('polluted')"]

After pollution, fork(script, args, options) can start the child with attacker-controlled Node flags. --eval is the interesting one: the child runs arbitrary JavaScript during startup. From that JavaScript you can call require("child_process").execSync(...) and run OS commands.

So the gadget is not “fork is always RCE”. The gadget is: pollute execArgv, then hit any code path that uses fork or spawn with an options object that inherits your value.

Prototype Pollution Basics click to expand

Prototype pollution exploits how JavaScript objects inherit properties. With controlled input, an attacker sets properties on an object’s prototype (the shared fallback other objects also use). Those objects then see the new properties even though they never defined them, which can change how the application behaves.

The usual high-impact target is Object.prototype, because most plain objects inherit from it. The attack does not always need that root, though.

Any prototype that sits on the chain of objects the application trusts is enough.

Polluting User.prototype, a shared config prototype, or another intermediate link can flip checks like if (user.isAdmin) for that lineage alone, even while unrelated objects stay unaffected.

Prototype pollution can happen client-side in the browser, or server-side if the backend runs a JavaScript runtime (usually Node.js).

Example payloads

These are common shapes attackers send when an app merges or parses user input unsafely.

Via __proto__:

{
  "__proto__": {
    "isAdmin": true
  }
}

Same idea in a query or URL (some client-side parsers turn nested keys into objects):

?__proto__[isAdmin]=true
#__proto__[isAdmin]=true

Via constructor.prototype:

{
  "constructor": {
    "prototype": {
      "isAdmin": true
    }
  }
}

Payload explaination:

Every object has a constructor that points at the function used to create it. Writing to constructor.prototype pollutes that function’s prototype, so later instances inherit the property.

class User {
  username = ""
}

const user1 = new User()

user1.constructor.prototype.isAdmin = true // same as: User.prototype.isAdmin = true

const user2 = new User()

console.log(user2.isAdmin) // true

const somethingElse = {}

console.log(somethingElse.isAdmin) // undefined (different prototype chain)

SSRF & Open Redirection

Open Redirection Vulnerabilities click to expand

What is Open Redirection?

Open redirection is a vulnerability where an application accepts user-controlled input that determines where to redirect the user, without properly validating the destination URL. This allows attackers to redirect users to malicious websites.

How open redirection works:

  1. Application has a redirect endpoint like ?redirect=https://example.com
  2. The parameter value is used directly in a Location header or JavaScript redirect
  3. Attacker replaces the URL with a malicious site
  4. Users are redirected to the attacker’s site

Common vulnerable patterns:

GET /redirect?url=https://<attacker-domain>.com
GET /login?return=https://<attacker-domain>.com
GET /next?target=https://<attacker-domain>.com

Why open redirection is dangerous:

  • Phishing attacks - redirect users to fake login pages
  • Bypassing filters - use the legitimate domain to bypass URL filters
  • Chain attacks - combine with other vulnerabilities like SSRF
Server-Side Request Forgery (SSRF) click to expand

What is Server-Side Request Forgery (SSRF)?

Server-Side Request Forgery (SSRF) is a vulnerability that allows an attacker to make the server-side application send HTTP requests to an arbitrary domain of the attacker’s choosing. This can be used to:

  • Access internal services that are not directly exposed to the internet
  • Bypass authentication by accessing internal admin interfaces
  • Read sensitive files from internal systems
  • Extract data from internal databases or APIs

How SSRF works:

  1. The application accepts a URL as a parameter (example: ?url=https://example.com)
  2. The server makes an HTTP request to that URL
  3. The server processes the response (blind SSRF) or returns it to the user (non-blind SSRF)
  4. An attacker replaces the URL with an internal address (example: http://localhost/admin)

Common targets for SSRF:

  • localhost or 127.0.0.1 - the server itself
  • 192.168.x.x or 10.x.x.x - internal network ranges
  • 169.254.x.x - AWS metadata service
  • Internal APIs, databases, or admin panels
SSRF + Open Redirection Chain Attack click to expand

Open Redirection + SSRF Chain Attack

When an application can make arbitrary requests with SSRF protections and also has an open redirection vulnerability, attackers can chain these to bypass the SSRF filters:

Example attack flow:

  1. SSRF protection blocks: http://localhost/admin - direct internal URLs are filtered
  2. But allows: https://<victim-domain>/redirect?url=http://localhost/admin - external domains (victim’s domain) pass validation
  3. Open redirection redirects: The victim’s site redirects the request to the internal target since it’s made server-side
  4. Result: SSRF to internal admin interface bypassed through request redirection making the internal request

Why this works:

  • The SSRF filter only checks if the initial URL is “safe” (external domain)
  • It doesn’t follow redirects to see the final destination
  • The open redirection vulnerability acts as a proxy to reach internal targets