Hello again,
This is a short tl;dr of the account lockout bypass via case permutation vulnerability
tracked as CVE-2025-6004 outlined in cyala.ai - Cracking the Vault.
Here is the tl;dr:
- Account lockouts are tracked in a map, where the keys contain the case-sensitive username originating from the request.
- Login attempts perform user lookups based on the lower-cased username.
- By altering the case of the username the account lockout mechanism is bypassed while still targeting the correct user account.
To demonstrate, the table below highlights the username field and how it is seen
by the lockout handler and the login handler:
| Request | Lockout Handler | Login Handler |
|---|
| admin | admin | admin |
| ADMIN | ADMIN | admin |
| AdMiN | AdMiN | admin |
Identifying this flaw by just reading the Vault source code was quite a
challenge for me. I’ve yet a lot to learn reading non-trivial go apps. That
being said, I decided to write a boiled down version of the vulnerability for
shits and giggles:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
| // tl;dr CVE-2025-6004
package main
import (
"errors"
"fmt"
"strings"
)
type Locker struct {
storage map[string]int
}
func (r *Locker) Increment(username string) {
_, ok := r.storage[username]
if !ok {
r.storage[username] = 1
} else {
r.storage[username]++
}
}
func (r *Locker) IsLocked(username string) bool {
failedAttempts, ok := r.storage[username]
if !ok {
fmt.Printf("[lock-check] username=%#v is NOT locked.\n", username)
return false
}
if failedAttempts < 3 {
fmt.Printf("[lock-check] username=%#v is NOT locked.\n", username)
return false
}
fmt.Printf("[lock-check] username=%#v is locked.\n", username)
return true
}
type User struct {
Username string
Password string
}
type LoginLocker struct {
locker Locker
userStorage map[string]*User
}
func (ll *LoginLocker) Login(username string, password string) error {
// lock check is performed on the **case-sensitive** username.
if ll.locker.IsLocked(username) {
return errors.New("user account locked")
}
// Fetch user based on **case-insensitive** username.
userKey := strings.ToLower(username)
user, ok := ll.userStorage[userKey]
if !ok {
return errors.New("invalid username or password")
}
if user == nil || user.Password != password {
ll.locker.Increment(username)
return errors.New("invalid username or password")
}
return nil
}
func main() {
l := LoginLocker{
locker: Locker{storage: make(map[string]int, 0)},
userStorage: map[string]*User{
"admin": {Username: "admin", Password: "correct"},
},
}
// Trigger account lock.
for range 4 {
err := l.Login("admin", "admin")
if err != nil {
fmt.Printf("failed to authenticate: %s\n", err.Error())
}
}
// Bypass the account lock by switching username case.
err := l.Login("Admin", "correct")
if err != nil {
fmt.Printf("failed to authenticate: %s\n", err.Error())
}
}
|
’til next time.