┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Password Profiler — picoCTF 2026
~ Imattas aka Zemi
 Category: General Skills
 Difficulty: Easy
 Points: 100
 Author: Imattas aka Zemi

────────────────────────────────────────────────────────────────────────────────

--[ Challenge Description ]--

 We intercepted a suspicious file from a system, but instead of the password
itself, it only contains its SHA-1 hash. Using publicly available breach data,
can you identify the original password?

────────────────────────────────────────────────────────────────────────────────

--[ Recon / Initial Analysis ]--

This challenge tests the ability to reverse a SHA-1 hash back to its plaintext
password using publicly available breach databases. The concept is based on the
real-world scenario where leaked password databases (from data breaches) can be
used to look up hashes and recover the original passwords.

The provided file contains a SHA-1 hash (40 hex characters). Read it first:
-- bash --
cat hash.txt
Example contents:
5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
:: Key Concepts

SHA-1 Hashing: SHA-1 (Secure Hash Algorithm 1) produces a 40-character
hexadecimal digest. While SHA-1 is a one-way function (you cannot mathematically
reverse it), weak or commonly-used passwords can be recovered through:
- Rainbow tables: Precomputed tables mapping hashes to plaintext
- Breach databases: Collections of passwords from real-world data breaches
- Online lookup services: Services like CrackStation, Have I Been Pwned (Pwned
Passwords), and others

Breach Data Lookup: The description explicitly mentions "publicly available
breach data," pointing to services like:
- Have I Been Pwned - Pwned Passwords API: Contains over 800 million compromised
passwords, searchable by SHA-1 hash. The API uses a k-anonymity model where you
send only the first 5 characters of the SHA-1 hash.
- CrackStation: An online hash lookup service with a massive precomputed
database supporting MD5, SHA-1, SHA-256, and other hash types.
- Hashcat / John the Ripper: Offline cracking tools that can be used with
wordlists from breach data.

────────────────────────────────────────────────────────────────────────────────

--[ Vulnerability / Observation ]--

SHA-1 is mathematically one-way, but the password it protects is weak and
present in public breach corpora. Because the plaintext was a commonly-used
password, the hash can be reversed by lookup rather than brute force. The
recovered plaintext IS the flag content (wrapped in picoCTF{} format).

:: The Attack

1. Read the provided file to obtain the SHA-1 hash
2. Look up the hash in a breach database or hash lookup service
3. The plaintext password IS the flag (wrapped in picoCTF{} format)

────────────────────────────────────────────────────────────────────────────────

--[ Exploitation / Solution ]--

:: Step 1: Obtain the SHA-1 Hash
Download and read the challenge file:
-- bash --
cat hash.txt
This file contains a SHA-1 hash (40 hex characters), for example:
5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
:: Step 2: Look Up the Hash Using Online Services

Method A: CrackStation (easiest)
1. Go to https://crackstation.net/
2. Paste the SHA-1 hash
3. Submit -- the plaintext password is returned

Method B: Have I Been Pwned - Pwned Passwords API
The HIBP API uses k-anonymity -- you send only the first 5 hex chars of the
SHA-1 hash:
-- bash --
# If the hash is 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
# Send first 5 chars: 5BAA6
curl https://api.pwnedpasswords.com/range/5BAA6
This returns a list of hash suffixes. Search for your hash's suffix in the
response.

Method C: hashcat with breach wordlist
-- bash --
hashcat -m 100 hash.txt /usr/share/wordlists/rockyou.txt
:: Step 3: Construct the Flag
The recovered plaintext password is the flag content. Wrap it in the picoCTF
flag format:
picoCTF{<recovered_password>}
────────────────────────────────────────────────────────────────────────────────

--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Password Profiler - picoCTF 2026
Category: General Skills | Points: 100

Automated solution to crack a SHA-1 hash using publicly available breach
data sources (Have I Been Pwned Pwned Passwords API, CrackStation-style
lookups, and local wordlist cracking).

Usage:
    python3 solve.py [hash_file]
    python3 solve.py                      # reads 'hash.txt' by default
    python3 solve.py challenge_hash.txt   # reads from specified file
    SHA1_HASH=<hash> python3 solve.py     # direct hash via env var

Dependencies:
    pip install requests
"""

import os
import sys
import re
import hashlib
import requests

FLAG_PATTERN = re.compile(r"picoCTF\{[^}]+\}")
SHA1_PATTERN = re.compile(r"^[0-9a-fA-F]{40}$")


def read_hash_from_file(filepath):
    """Read a SHA-1 hash from a file."""
    try:
        with open(filepath, "r") as f:
            content = f.read().strip()
        # The file might contain just the hash, or have extra text
        for line in content.splitlines():
            line = line.strip()
            if SHA1_PATTERN.match(line):
                return line.lower()
        # If no pure hash line found, try to find a hash anywhere
        match = re.search(r"[0-9a-fA-F]{40}", content)
        if match:
            return match.group(0).lower()
        print(f"[!] No SHA-1 hash found in {filepath}")
        print(f"    File contents: {content[:200]}")
        return None
    except FileNotFoundError:
        print(f"[!] File not found: {filepath}")
        return None


def lookup_hibp(sha1_hash):
    """
    Look up a SHA-1 hash using the Have I Been Pwned Pwned Passwords API.
    Uses k-anonymity: only sends first 5 characters of the hash.
    Returns the plaintext password if found, None otherwise.

    Note: The HIBP API expects the SHA-1 of the PASSWORD, and returns
    hash suffixes. If the challenge provides a SHA-1 hash of a breached
    password, we check if that hash appears in the HIBP database.
    """
    sha1_upper = sha1_hash.upper()
    prefix = sha1_upper[:5]
    suffix = sha1_upper[5:]

    print(f"[*] Querying HIBP Pwned Passwords API (prefix: {prefix}...)")
    try:
        resp = requests.get(
            f"https://api.pwnedpasswords.com/range/{prefix}",
            headers={"User-Agent": "picoCTF-solver"},
            timeout=10,
        )
        if resp.status_code == 200:
            for line in resp.text.splitlines():
                parts = line.strip().split(":")
                if len(parts) == 2:
                    hash_suffix, count = parts
                    if hash_suffix.strip() == suffix:
                        print(f"[+] Hash found in HIBP! Seen {count.strip()} times in breaches.")
                        print("[*] HIBP confirms this is a known breached password hash.")
                        print("[*] The hash exists in breach data -- now we need the plaintext.")
                        return True  # Hash exists but HIBP doesn't give us plaintext
            print("[-] Hash not found in HIBP database.")
        else:
            print(f"[!] HIBP API returned status {resp.status_code}")
    except Exception as e:
        print(f"[!] HIBP lookup failed: {e}")

    return False


def lookup_crackstation_style(sha1_hash):
    """
    Attempt to look up the hash using free online hash lookup APIs.
    Tries multiple services that provide reverse hash lookups.
    """
    services = [
        {
            "name": "nitrxgen",
            "url": f"https://www.nitrxgen.net/md5db/{sha1_hash}",
            "parser": lambda r: r.text.strip() if r.text.strip() else None,
        },
        {
            "name": "hashtoolkit",
            "url": f"https://hashtoolkit.com/reverse-sha1-hash/?hash={sha1_hash}",
            "parser": lambda r: extract_from_html(r.text),
        },
    ]

    for service in services:
        print(f"[*] Trying {service['name']}...")
        try:
            resp = requests.get(
                service["url"],
                headers={"User-Agent": "Mozilla/5.0"},
                timeout=10,
            )
            if resp.status_code == 200:
                result = service["parser"](resp)
                if result:
                    print(f"[+] Found via {service['name']}: {result}")
                    return result
        except Exception as e:
            print(f"[!] {service['name']} failed: {e}")

    return None


def extract_from_html(html):
    """Extract plaintext from HTML response of hash lookup services."""
    # Look for common patterns in hash lookup result pages
    patterns = [
        re.compile(r'<span class="res">(.*?)</span>'),
        re.compile(r'class="text-success"[^>]*>(.*?)<'),
        re.compile(r"Decrypted.*?:\s*(.*?)(?:<|$)"),
        re.compile(r"Result.*?:\s*(.*?)(?:<|$)"),
    ]
    for pattern in patterns:
        match = pattern.search(html)
        if match:
            result = match.group(1).strip()
            if result and len(result) < 100:
                return result
    return None


def crack_with_wordlist(sha1_hash, wordlist_paths=None):
    """
    Attempt to crack the hash using local wordlists.
    Computes SHA-1 of each word and compares.
    """
    if wordlist_paths is None:
        wordlist_paths = [
            "/usr/share/wordlists/rockyou.txt",
            "/usr/share/wordlists/rockyou.txt.gz",
            "/usr/share/seclists/Passwords/Common-Credentials/10-million-password-list-top-1000000.txt",
            "/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt",
            "/usr/share/john/password.lst",
            "rockyou.txt",
            "wordlist.txt",
            "passwords.txt",
        ]

    for wordlist in wordlist_paths:
        if not os.path.exists(wordlist):
            continue

        print(f"[*] Cracking with wordlist: {wordlist}")

        open_func = open
        if wordlist.endswith(".gz"):
            import gzip
            open_func = gzip.open

        try:
            with open_func(wordlist, "rb") as f:
                count = 0
                for line in f:
                    try:
                        word = line.strip().decode("utf-8", errors="ignore")
                    except Exception:
                        continue

                    computed_hash = hashlib.sha1(word.encode("utf-8")).hexdigest()
                    if computed_hash == sha1_hash:
                        print(f"[+] CRACKED! Plaintext: {word}")
                        return word

                    count += 1
                    if count % 1000000 == 0:
                        print(f"    ...tried {count:,} passwords")

            print(f"[-] Not found in {wordlist} ({count:,} entries checked)")
        except Exception as e:
            print(f"[!] Error reading {wordlist}: {e}")

    return None


def try_common_passwords(sha1_hash):
    """Try a small built-in list of extremely common passwords."""
    print("[*] Trying common passwords...")
    common = [
        "password", "123456", "12345678", "qwerty", "abc123", "monkey",
        "1234567", "letmein", "trustno1", "dragon", "baseball", "iloveyou",
        "master", "sunshine", "ashley", "michael", "shadow", "123123",
        "654321", "superman", "qazwsx", "michael", "football", "password1",
        "password123", "batman", "login", "admin", "welcome", "hello",
        "charlie", "donald", "starwars", "1234567890", "123456789",
        "00000000", "0000", "1111", "1234", "passwd", "pass", "test",
        "guest", "access", "p@ssw0rd", "P@ssw0rd", "P@ssword1",
        "picoctf", "picoCTF", "flag", "secret", "hidden",
    ]

    for word in common:
        computed = hashlib.sha1(word.encode("utf-8")).hexdigest()
        if computed == sha1_hash:
            print(f"[+] CRACKED! Plaintext: {word}")
            return word

    print(f"[-] Not in common password list ({len(common)} entries)")
    return None


def main():
    print("=" * 60)
    print("  Password Profiler - picoCTF 2026 Solver")
    print("  Category: General Skills | Points: 100")
    print("=" * 60)
    print()

    # Determine the SHA-1 hash to crack
    sha1_hash = os.environ.get("SHA1_HASH", "").strip().lower()

    if not sha1_hash:
        # Try to read from file
        hash_file = sys.argv[1] if len(sys.argv) > 1 else "hash.txt"
        sha1_hash = read_hash_from_file(hash_file)

    if not sha1_hash:
        print("[!] No SHA-1 hash provided.")
        print(f"    Usage: python3 {sys.argv[0]} [hash_file]")
        print(f"    Or:    SHA1_HASH=<hash> python3 {sys.argv[0]}")
        sys.exit(1)

    if not SHA1_PATTERN.match(sha1_hash):
        print(f"[!] Invalid SHA-1 hash format: {sha1_hash}")
        print("    Expected: 40 hexadecimal characters")
        sys.exit(1)

    print(f"  Target SHA-1: {sha1_hash}")
    print()

    # Phase 1: Try common passwords first (instant)
    plaintext = try_common_passwords(sha1_hash)

    # Phase 2: Check HIBP to confirm it's a breached password
    if not plaintext:
        lookup_hibp(sha1_hash)

    # Phase 3: Try online hash lookup services
    if not plaintext:
        plaintext = lookup_crackstation_style(sha1_hash)

    # Phase 4: Try local wordlist cracking
    if not plaintext:
        plaintext = crack_with_wordlist(sha1_hash)

    # Results
    if plaintext:
        flag = f"picoCTF{{{plaintext}}}"

        # Check if the flag was already in picoCTF format
        if FLAG_PATTERN.match(plaintext):
            flag = plaintext

        print()
        print("=" * 60)
        print(f"  Cracked password: {plaintext}")
        print(f"  Flag: {flag}")
        print("=" * 60)
        print()
        print("[*] Note: The flag format may vary. Try both:")
        print(f"    - {flag}")
        print(f"    - {plaintext}")
    else:
        print()
        print("[-] Could not crack the hash automatically.")
        print("[*] Manual steps to try:")
        print(f"    1. Go to https://crackstation.net/ and paste: {sha1_hash}")
        print(f"    2. Go to https://hashes.com/en/decrypt/hash and paste: {sha1_hash}")
        print(f"    3. Use hashcat: hashcat -m 100 '{sha1_hash}' /path/to/rockyou.txt")
        print(f"    4. Use john: echo '{sha1_hash}' > hash.john && john --format=raw-sha1 hash.john --wordlist=/path/to/rockyou.txt")


if __name__ == "__main__":
    main()
────────────────────────────────────────────────────────────────────────────────

--[ Key Takeaways ]--

- SHA-1 is one-way mathematically, but weak/common passwords are trivially
recoverable via breach corpora and precomputed lookups.
- The Have I Been Pwned Pwned Passwords API uses a k-anonymity model: only the
first 5 hex characters of the hash are sent, and the response is searched for
the matching suffix.
- CrackStation and similar online services provide instant reverse lookups for
common hashes.
- Offline cracking with hashcat -m 100 (raw SHA-1) or John the Ripper against
rockyou.txt is the fallback when online lookups miss.
- The recovered plaintext is the flag content -- wrap it in picoCTF{}.