Backstory#
Recently, I found myself stumbling upon a .kdbx
(
KeePass Database) file as part of a backup in a CTF and needed to crack the password to gain access to the secrets contained and consequently elevate my privileges.
Problem was that I couldn’t get the hash in the right format for cracking it with john
:
unsupported database file version (4)
And y’all know what that means:
Let’s go!
⚠️ Disclaimer: This post is for educational and ethical purposes only. It’s meant to help readers understand penetration testing and improve security. Do not use these techniques without explicit permission from the system owner. Unauthorized access or tampering is illegal, and the author is not responsible for misuse.
What’s .kdbx
anyhow?#
A .kdbx
file is an encrypted database used by KeePass and compatible password managers to securely store logins, notes, and other secrets. It uses strong encryption (like AES-256 or ChaCha20) with a key derived from your master password and optional key files, making it highly resistant to brute force attacks and tampering.
Game Plan#
Since both of my reliable old friends hashcat
and john
took me nowhere in this case and a quick search also did not lead to feasible alternatives, this was screaming for some crude hands-on solution. Conceptionally, this would be really simple: going over a list of words and trying to open up the database with each one of them until it would work (cough brute force anyone?).
Basics#
So, first we need some kind of way to interact with the KeePass format. My go-to choice of language is usually … Go (Ba Dum Tss!), as it’s simple enough for me to comprehend while still offering more than decent performance. After a quick online search, I found github.com/tobischo/gokeepasslib/v3
, so the path of tackling this task with my favorite language was paved and ready to be walked.
As shown in the following snippet, usage is also simple enough without any surprises. We just go read the database file in memory, create a new Credential with gokeepasslib.NewPasswordCredentials
and then decode with db.UnlockProtectedEntries
:
db := gokeepasslib.NewDatabase()
db.Credentials = gokeepasslib.NewPasswordCredentials(word)
_ = gokeepasslib.NewDecoder(dbRead).Decode(db)
err := db.UnlockProtectedEntries()
For effective bruteforcing, we still need a way to know whether our (solely in good will) provided credentials were correct or not. Thankfully the error that is potentially returned by the function call to unlock the database entries will tell us just that:
Cannot read database: Either credentials are invalid or the database file is corrupted
When we find the right password, no error will be returned and we can print a winner. That’s already it. We gonna iterate over our word list and try connecting until we don’t encounter an error. Having come that far, we can go ahead and leverage the capabilities of gokeepass
to print out all entries of all groups in the content root:
if err == nil {
log.Printf("we got a winner: %v", word)
groups := db.Content.Root.Groups
for _, grp := range groups {
entries := grp.Entries
for _, ent := range entries {
log.Println(ent.GetTitle())
log.Println(ent.GetPassword())
}
}
}
To find the final result of keepass-rush
(yeah, that’s how I called it), just head over to my GitHub.
Extra: Making it less stupid#
Having the super naive and crude POC going, there were still 2 key optimizations for increasing performance and enabling parallel workers for speeding up the whole process even more:
- reading the database file into memory only once
- enable multiple parallel workers
The former is a relatively easy fix. We can simply read the whole file in memory at the start of the program:
dbFile, _ := os.Open(*path)
dbData, _ := io.ReadAll(dbFile)
Upon passing the data to gokeepasslib
, we can then create a new bytes.Reader
for each attempt. This is necessary for concurrent reads, because each bytes.Reader
will keep its own read offset.
dbRead := bytes.NewReader(*dbData)
...
_ = gokeepasslib.NewDecoder(dbRead).Decode(db)
For the second optimization we leverage a classic Go multi threading pattern using goroutines <3 and channels. For that, we will create a buffered channel and spawn the number of parallel workers that we have configured, while passing in a reference to the database we already have in memory and the channel for synchronization.
wordsCh := make(chan string, *parallel)
...
for i := 1; i <= *parallel; i++ {
wg.Add(1)
go worker(&dbData, wordsCh, &wg)
}
Then, in an additional goroutine we will just iterate over all entries in the given wordlist and send each entry to the buffered channel.
scanner := bufio.NewScanner(wordFile)
go func() {
cnt := 0
for scanner.Scan() {
cnt++
line := scanner.Text()
wordsCh <- line
}
close(wordsCh)
}()
This way, each worker will retrieve the next word to try as soon as it’s available and they can work in parallel without handling any wordlist entries more than exactly once.
Fin
That’s it for today and don’t forget kids:
🔥 Nobody get Pen’ned without the Consent!