September 19, 2026
My First Bug Bounty Finding Was Critical: Exploiting a Weak JWT Secret for Account Takeover
This was my first real bug bounty finding, and somehow, it turned into a Critical vulnerability with a $1,500 bounty.

By yOnly
7 min read
What started with a simple JWT secret that looked almost too obvious eventually led to a full Account Takeover. But when I first saw that secret, I didn't even know whether it was actually useful or just another interesting finding.
I didn't have a clear idea of where the investigation would lead. I simply kept asking myself one question: "What can I actually do with this?"
This is the story of how I found my first bug bounty.
Starting With Recon
During my internship at a cybersecurity company, we were asked to start by getting familiar with the OWASP Web Security Testing Guide (WSTG). As part of the learning process, we were also given a bug bounty scope to practice on, using a wildcard program on YesWeHack as our target.
The first thing I did was figure out which subdomain would be the most interesting place to apply what I had learned. I was looking for something that had a fairly complete set of features, especially the basics like registration and login, so I could actually follow the application's workflow and put the WSTG into practice.
After some recon using tools like VirusTotal, Subfinder, Asset-finder, httpx, and katana, I eventually came across a subdomain that looked like a good fit. I didn't waste much time and started going through the WSTG methodology. At one point, I reached the part where I needed to create an account through the registration feature and then log in, mainly to get a better understanding of how the application worked and how its authentication flow was structured.
The JWT Secret Was ''default''
After successfully logging in, one thing immediately caught my attention. The JWT extension in Burp Suite was able to detect the secret key being used to sign the token β and surprisingly, the secret was simply "default".
At that point, I honestly didn't fully understand what this meant or why it mattered. So instead of immediately jumping to conclusions, I decided to take a step back and learn how JWT signing actually works.
One of the first things I looked into was the difference between the two commonly encountered JWT signing algorithms: HS256 and RS256.
HS256 is a symmetric signing algorithm. It uses the same secret key to both create and verify a JWT signature. In a simplified way, the server takes the JWT's header and payload, combines them with the secret key, and generates a signature using HMAC-SHA256. When the server receives the token later, it uses that same secret to calculate the signature again and checks whether it matches the one included in the token.
RS256 works quite differently. It is an asymmetric algorithm that uses a pair of keys: a private key for signing and a public key for verification. The private key is supposed to remain secret, while the public key can be distributed to systems that need to verify the token.
This difference turned out to be very important in my case.
Because the application was using HS256, the security of the JWT ultimately depended on keeping the shared secret confidential. The secret itself is not normally stored inside the JWT. Instead, it is used behind the scenes to generate the signature. However, if the secret is weak or predictable, an attacker can potentially guess it by trying a large number of candidate values until one produces a valid signature.
In my case, the extension had already identified the secret as "default". That made me curious about how the extension was able to find it in the first place.
It wasn't actually extracting the secret from the JWT itself. Rather, the process is closer to secret guessing and verification. Since HS256 requires the secret to generate a valid signature, a candidate secret can be tested against the token. If the generated signature matches the signature in the JWT, the candidate is the correct secret.
This also explained something else I had initially misunderstood: the JWT payload itself does not contain the secret key. Anyone can decode the header and payload of a JWT because Base64URL encoding is not encryption. The important part is the signature, which is what allows the server to verify that the token was generated using the expected secret.
After understanding this, I wanted to see whether the same process could be performed manually instead of relying on the Burp Suite extension.
It turns out it could. Tools such as Hashcat can be used to test candidate secrets against JWTs. For example:
hashcat -a 0 -m 16500 <jwt_token> <path/to/wordlist/jwt-signature-key-common-wordlists.txt>hashcat -a 0 -m 16500 <jwt_token> <path/to/wordlist/jwt-signature-key-common-wordlists.txt>For the wordlist, I used a publicly available collection from GitHub:
jwt-secrets/jwt.secrets.list at master Β· wallarm/jwt-secrets Contribute to wallarm/jwt-secrets development by creating an account on GitHub.
At this point, what initially looked like a small and rather confusing finding β a JWT extension showing "default" β started to make much more sense. I was beginning to understand that the real issue wasn't simply that a secret had been discovered, but that the application was relying on a predictable secret for signing its JWTs.
From a Weak Secret to Account Takeover
In bug bounty, finding a secret key like this doesn't automatically mean you've found a vulnerability. What really matters is the impact. A weak secret is interesting, but I needed to figure out what an attacker could actually do with it.
Since I had already confirmed that the JWT secret was indeed "default", I decided to see whether I could manipulate the token and use it for something meaningful. I went to JWT.io and started playing around with the JWT payload.
One of the things I initially had in mind was trying to chain this into a vertical privilege escalation. During my reconnaissance, I had discovered that a "super_admin" role existed, and I also found the general format used for the role-related value.
Unfortunately, none of the combinations I tried worked.
The JWT seemed to only accept values for claims that were already present in the original payload. At first, this felt like a dead end. But then I noticed something much more interesting: the token contained a UserId.
And that changed everything.
If the JWT was being used as part of the application's authentication and authorization mechanism, and the userId inside the token determined which account was being accessed, then being able to generate a valid JWT for another user's ID could potentially lead to a full Account Takeover.
So, rather than continuing to guess the privilege-related values, I decided to test this possibility properly.
First, I created two separate accounts:
User A β UserId = 100184
User B β UserId = 100185
I also looked for a sensitive endpoint that returned user information. I found the user detail API endpoint, which looked like a good candidate for testing whether the JWT actually controlled which account the server would return.
I started by logging in as User A and accessing the user detail endpoint.
I then copied the JWT generated for User A and put it into JWT.io so I could take a closer look at its structure.
As you can see in the screenshot above, the payload was surprisingly simple. It contained only two claims: UserId and UserType.
The interesting one was obviously UserId.
Before changing anything, I also wanted to confirm that the secret I'd discovered earlier was actually valid. So I entered "default" as the signing secret in JWT.io.
And it worked.
That meant I could not only read the token's payload, but also generate a new valid signature using the same secret.
So I changed the UserId from User A's ID to User B's:
100184 β 100185
I then encoded the modified payload using the "default" secret and copied the newly generated JWT.
Finally, I replaced the original token in the request to the user detail endpoint with the manipulated one.
And then⦠boom!.
The application returned User B's information.
The Impact
At that moment, the impact became clear.
I had started with what initially looked like a simple case of a weak JWT secret. But because that secret allowed me to generate a valid token with a different UserId, I could effectively make the application treat me as another user.
In other words, I could take a JWT belonging to my own account, change the userId to another user's ID, sign it again using the "default" secret, and have the application accept it as a legitimate authentication token.
That turned a seemingly simple JWT misconfiguration into a full Account Takeover vulnerability.
I reported the vulnerability through YesWeHack, and the report was eventually rated Critical, with a $1,500 bounty.
The Lesson
Looking back, this was a pretty interesting lesson for me. At first, I was honestly just curious about why Burp Suite's JWT extension was showing "default" as the secret. I didn't immediately know whether it would lead anywhere.
The important part was not stopping at the initial finding.
A weak secret by itself was only the beginning. The real story started when I asked myself, "Okay, but what can I actually do with this?"
That question eventually led me from a weak JWT secret to manipulating the authentication token and, ultimately, taking over another account.
It was also a good reminder that seemingly small security mistakes can sometimes have consequences far beyond what you initially expect. In this case, using a predictable value like "default" as a JWT signing secret turned out to be enough to completely break the application's trust in its authentication tokens.
And, for me personally, this was a pretty memorable way to get my first real bug bounty finding.