This is not a guide about bypassing login.
It is a reverse engineering write-up: how I started from an ordinary login page, followed the frontend bundles to the API call chain, made a wrong assumption about a hash function, and eventually confirmed the answer by reading a few very familiar magic numbers.
Before We Start
One day, I opened liandanxia.io and wanted to answer a simple question:
What actually happens behind the login button of a modern frontend application?
The page itself did not look unusual. It had email/password login, phone login, GitHub OAuth, and Google OAuth. A pretty standard authentication setup.
But the more ordinary the page looked, the more curious I became about the implementation behind it.
So I set a very small scope for myself:
Do not touch accounts I do not own. Do not bypass captchas. Do not brute force anything. Only analyze the login flow exposed through publicly accessible frontend resources.
The toolset was simple:
| Tool | Purpose |
|---|---|
| Browser source / page fetching | Inspect what the homepage and login page expose |
| PowerShell / curl | Perform a small number of API checks |
| Select-String / grep | Search inside compressed frontend bundles |
| DeepSeek | Help read and reason about minified JavaScript |
The interesting part was not finding an API endpoint.
The interesting part was the hash function I almost misidentified.
First Clue: The API Base Was in the Nuxt Config
I started by fetching the homepage.
The response quickly revealed an important detail: this was a Nuxt 3 single-page application.
Inside the page source, I found a runtime config similar to this:
window.__NUXT__.config = {
public: {
apiBase: "https://api.liandanxia.io/silver",
region: "en"
}
}
There was nothing magical about this.
Many Nuxt, Vite, and Next-style applications expose public runtime configuration in the HTML. If the browser needs to call an API, the frontend must eventually know where that API lives.
So the first clue was clear:
API Base = https://api.liandanxia.io/silver
But knowing the API base was not enough.
The real questions were:
- Which function does the login button call?
- Is the password transformed before submission?
- Where does the token come back?
Second Clue: The Login Page Exposed the Bundles
Next, I opened the login page.
The login page was client-side rendered, so the raw HTML did not contain the actual form logic. But it did expose a list of Nuxt-generated JavaScript bundles:
<link rel="modulepreload" href="/_nuxt/j-XipXza.js">
<link rel="modulepreload" href="/_nuxt/HKoFWgYB.js">
<link rel="modulepreload" href="/_nuxt/c8TO03ko.js">
<link rel="modulepreload" href="/_nuxt/P1PMQwvn.js">
These filenames are content hashes, so they may change after every deployment. The names themselves are not stable clues. The useful approach is to follow whatever the current page actually loads.
After downloading a few candidate bundles, I found what looked like the login component.
The code was minified, and meaningful names had been replaced by short identifiers such as Le, Je, and Qe. Still, the Vue component structure was recognizable.
One part of the logic could be roughly reconstructed as:
async function submit() {
// Validate email and password
await loginWithEmail({
email: form.email,
password: form.password
})
}
The minified version was obviously less friendly:
await Le({
email: t.value.email,
password: t.value.password
})
The important part was not the name Le.
The important part was that Le came from the main entry bundle.
In other words, the login component was only the doorway. The real API implementation was hidden in the main bundle.
Third Clue: From Short Function Names to Real API Calls
The main bundle was a few hundred kilobytes, mostly compressed into a very long line of JavaScript.
This is not the kind of file you read from top to bottom.
You search it.
I started with obvious path patterns:
(Get-Content bundle.js -Raw) -split ';' |
Where-Object { $_ -match '/api/user' }
That quickly revealed the rough shape of the authentication-related endpoints.
I could see that login, registration, user info, OAuth, and verification flows were all close to the same authentication call chain.
But I still could not jump to a conclusion, because the login flow had one critical detail:
The frontend did not send the raw password directly. It hashed the password before submitting it.
That was where the story became much more interesting.
The Real Trap: I Thought It Was SHA-256
I kept tracing the login function.
In the minified code, the logic was roughly equivalent to this:
async function login(payload) {
const hashedPassword = encryptPassword(payload.password)
return requestLogin({
email: payload.email,
password: hashedPassword
})
}
So I followed encryptPassword.
Eventually, it pointed to a hash library wrapper with byte conversion, array handling, and hex output logic.
At that moment, I saw structures like:
new Uint8Array(...)
hash(...)
toString(...)
My first reaction was:
This is probably SHA-256.
Looking back, that was too quick.
But it was an easy mistake to make. In many modern projects, if developers hash something client-side, SHA-256 is often the first algorithm that comes to mind. The compressed code also looked like a generic hash wrapper, so I wrote a quick SHA-256 version and tested it.
The result was immediate:
Incorrect account or password
That meant at least one assumption was wrong.
Maybe the algorithm was wrong.
Maybe the encoding was wrong.
Maybe there was a salt.
Or maybe I had followed the wrong call chain.
The Funniest Part: MD5 Had Already Worked
Next, I did something boring but effective: I tested several common hashing schemes.
SHA-256, SHA-512, SHA-1, MD5, email-plus-password combinations, double hashing. I checked them in a small and controlled way.
One result stood out:
MD5 hex -> code 200
That should have been the answer.
But my test script had a bug.
It only looked for the token in one fixed location, while the real response nested the token under data.
So the request had succeeded, but my script failed to extract the token.
I looked at the "no token found" output and mistakenly concluded that MD5 was also wrong.
That was the most dramatic moment of the whole analysis:
The correct answer had already peeked through the door, and I closed the door on it because my own validation logic was broken.
When I looked back at this step later, I felt it was more valuable than simply finding the endpoint.
In reverse engineering, the most dangerous thing is not ignorance.
It is thinking you already know.
Back to the Source: The Magic Numbers Gave It Away
At that point, I decided to stop guessing.
I went back into the bundle and continued tracing deeper into the hash function. This time, I did not look at the shape of the wrapper. I looked for algorithm constants.
Then I saw these numbers:
-680876936
-389564586
606105819
And these rotations:
<< 7 | >>> 25
<< 12 | >>> 20
<< 17 | >>> 15
<< 22 | >>> 10
If you have ever read an MD5 implementation, these should look very familiar.
They match the classic constants and rotation schedule used in the MD5 round functions.
At that moment, the answer was finally clear:
The password was transformed into a standard MD5 hex digest before submission.
This became the main lesson of the whole exercise:
Do not identify an algorithm only by surface-level patterns like
Uint8Array,hash, ordigest.
Stronger evidence comes from constants, round functions, output behavior, and verification results all pointing to the same conclusion.
Another Reminder: Rate Limiting Was Real
During verification, I also triggered login rate limiting.
After several failed attempts, the API started returning remaining-attempt messages. After more failures, it reported that login was too frequent.
That means the endpoint was not completely unprotected. At minimum, it had basic protection against repeated failed login attempts.
It was also a reminder to keep this kind of research controlled:
Only test with an account you own. Keep the number of requests small. Do not turn the process into brute forcing, credential stuffing, automated registration, or captcha bypassing.
That would no longer be a technical write-up.
That would be crossing a line.
What I Confirmed
By the end of the analysis, I had confirmed a few things:
| Item | Finding |
|---|---|
| Frontend framework | Nuxt 3 / Vite build |
| API base | Exposed through Nuxt public runtime config |
| Login call chain | Login component calls authentication helpers from the main bundle |
| Password handling | Password is hashed as an MD5 hex digest before submission |
| Token location | Token is returned inside the login response data |
| Token type | JWT |
| Protection | Failed login rate limiting exists |
Three Lessons I Took Away
1. Public frontend assets often contain enough clues
SPA bundles can contain a surprising amount of runtime information.
API bases, endpoint paths, status-code handling, token storage keys, and third-party login entry points often do not require any "cracking" to discover. They are already loaded by the browser.
2. Guessing algorithms is risky
When I first saw the hash wrapper, I assumed it was SHA-256.
That assumption was too fast.
Compressed hash implementations often look similar. A more reliable approach is to go back to the algorithm itself: constants, round functions, output length, and verification behavior.
3. Test scripts can mislead you too
When MD5 first returned code 200, it had already pointed in the right direction.
But my script parsed the response incorrectly, so I misread a successful result as a failed one.
That kind of bug is subtle because it disguises a validation problem as a reverse engineering conclusion.
Final Thoughts
The most interesting part of this analysis was not finding an endpoint or writing a snippet of code.
It was being reminded again that:
Reverse engineering is rarely a straight line.
It feels more like walking through fog. You find clues, make assumptions, take wrong turns, and then suddenly stop in front of a tiny constant that tells you the answer had been there all along.
The next time I see a similar frontend login flow, I will probably slow down before naming the algorithm.
Do not rush to say, "This is probably SHA-256."
Find the magic numbers first.
I also put the full notes and supporting files in this GitHub repo if you want to take a closer look:
https://github.com/liandanxiaAI/reverse-engineering-daily/tree/main/liandanxia-login-analysis
Top comments (0)