Tally

Crack what was never meant to be cracked. Capture a real JWT on a small admin portal, brute-force its HS256 signing secret with a wordlist, forge a new token with elevated `role:admin` claims, and replay it against the admin exports endpoint.

Tally
Tally — figure 1

Room Description

There is a new feature on WebVerse called Foundational labs, there are meant to be easier than Easy and build some basic web exploitation skills.

This is information directly grabbed from the main page and description of the lab.

https://dashboard.webverselabs-pro.com/foundational-labs/tally

Synopsis

Crack what was never meant to be cracked.

What is Tally

A beginner-friendly Node.js + Express invoice-tracker SaaS that authenticates with HS256 JWTs in an Authorization: Bearer header. The signing secret is a single word from rockyou — no leak, no source disclosure, no exotic algorithm tricks. Capture a real token, crack it, forge one with role:admin, replay against /api/admin/exports.

Who is Tally for?

Newcomers comfortable with one or two injection labs who are ready to leave the SQL world for the auth-token world. The fifth WebVerse foundational, after Flower, Overdue, Corridor, and Quotin — and the first that introduces the modern web's most common auth primitive.

Skills / Knowledge

  • Decoding JWTs by hand and with jwt.io / jwt-cli
  • Spotting role / scope / is_admin claims as forgery targets
  • Running hashcat mode 16500 (or jwt-cracker) against a captured JWT
  • Re-signing a forged token in three lines of Node or Python
  • Replaying a forged token via Authorization: Bearer with curl

What will you gain?

  • Recognise a JWT for what it is — three base64url segments, the first two readable by anyone holding the token.
  • Understand the difference between a token being signed and a token being secret — HS256 with a weak password is structurally fine and semantically broken.
  • Use a wordlist-based cracker (hashcat -m 16500 or jwt-cracker) to recover an HS256 signing secret from a captured token.
  • Forge a token with elevated claims and replay it against a privileged endpoint with an Authorization: Bearer header.

Initial Analysis

When we try to browse to the provisioned IP we get an error:

Tally — figure 2

We can't resolve this domain, to fix this, we need to add the IP address given to us, and the domain name we're attempting to resolve to /etc/hosts.

On Windows the file is located at C:\Windows\System32\drivers\etc\hosts , on Linux it's just /etc/hosts. If you have issues editing the file on Windows, just create a new text file wherever, edit it there, then copy and paste it to the location to override the old one. To do so, you do need Local Admin or sudo permissions.

After doing so, we can try to refresh and we can see that we now have access to the web app.

Tally — figure 3

We have several endpoints to look through, and even though we can create an account of our own, this time there is information we need from the unauthenticated segment:

    <nav class="nav-links">
      <a href="/about">About</a>
      <a href="/pricing">Pricing</a>
      <a href="/changelog">Changelog</a>
      <a href="/login">Log in</a>
      <a href="/signup" class="nav-cta">Start free →</a>
    </nav>

The about page just tells us generic information about the application and the creators:

Tally — figure 4

The pricing seems modest enough:

Tally — figure 5

Now, the changelog definitely holds interesting information that we will need for the future:

Tally — figure 6

That admin only endpoint is definitely the final target for an authentication based lab such as this one. The signup form looks as follows:

Tally — figure 7

Finding the bug

Let's create an account.

Tally — figure 8

Upon account creation we get assigned a token, a JWT specifically.

Tally — figure 9

So we have a very meaningful value in our JWT called role, ideally we would want it to say admin, and not user.

The dashboard landing page is pretty dull with nothing to offer, just like Invoices and Clients, there is just no useful information around there.

Tally — figure 10

There are two API calls happening when we login, one towards /api/dashboard, but as we saw there is 0 information we need there and a second on towards /api/auth/me.

Tally — figure 11

Well, I guess it's time to look into our JWT, we know it's HS256 signed, and there are a bunch of JWT attacks that could be possible, such as:

Tally — figure 12
Tally — figure 13

But considering the lab description, we know it's a weak signature.

https://portswigger.net/web-security/jwt#brute-forcing-secret-keys

Exploitation

There are two ways that we can solve it, or well, a bunch of ways, but two ways that are easy, using hashcat with mode 16500 or using the cracking module from jwt_tool.

python3 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoibWluYXRvdXJAZ21haWwuY29tIiwibmFtZSI6Im1pbmF0b3VyIiwicm9sZSI6InVzZXIiLCJpYXQiOjE3Nzk1ODE0MjYsImV4cCI6MTc4MDE4NjIyNn0.GAqSAof7p4xWmKNYydKLnd7o23oeuwky2ROmteOFJ78 -C --dict /usr/share/wordlists/rockyou.txt
Tally — figure 14

We have successfully cracked the secret, so we can tamper with the values in the payload either through jwt_tool or jwt.io:

python3 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjMsImVtYWlsIjoibWluYXRvdXJAZ21haWwuY29tIiwibmFtZSI6Im1pbmF0b3VyIiwicm9sZSI6InVzZXIiLCJpYXQiOjE3Nzk1ODE0MjYsImV4cCI6MTc4MDE4NjIyNn0.GAqSAof7p4xWmKNYydKLnd7o23oeuwky2ROmteOFJ78 -T -S hs256 -p "tally123"

We just follow the prompts and when we see that the payload values show up, we select the one we want to change and insert a new value.

Tally — figure 15

To verify that we now have a manipulated JWT we can check it on jwt.io

Tally — figure 16

Now, the information we got from the changelog, we know exactly which endpoint to target, but in the odd case that we didn't, we could fuzz /api and /api/admin and get the hidden endpoint.

Tally — figure 17

This would be a dead end, but:

Tally — figure 18

This would give us the money.

Tally — figure 19

Bam wham, thank you mam. Usually, after forging a JWT, we can interact with the web application by just changing the token in either local storage or cookies through DevTools to maintain a persistent session, but this time around that didn't work, it seems like only the endpoint specified is vulnerable.