← Back to Writeups
CUSTOMMediumMisc

Reverse Wordle

XESXOR9/2/20263 min read
#misc#custom#medium#api_research#constraint_solving#game_mechanics_analysis

Reverse Wordle

Platform: uoftctf2026 | Category: Misc | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-01-10 | Status: Solved Techniques: api_research, constraint_solving, game_mechanics_analysis

Summary

Task: Given Wordle share patterns from three games with the same unknown starting word, find that word and submit its SHA256 hash. Solution: Looked up historical Wordle answers via the NYT API for games 1, 67, and 1336, then brute-forced the 5-letter word dictionary to find SQUIB matching all three color patterns.

Reverse Wordle - UofTCTF 2026

Description

My friend said they always use the same starting word, can you help me find out what it is? Submit the sha256 hash of the ALL CAPS word wrapped in the flag format uoftctf{...}

We're given a file chall.txt containing Wordle share results from three different games.

Analysis

Challenge Files

The file contains three Wordle game results:

Wordle 1 3/6*

ā¬›ā¬›šŸŸØā¬›šŸŸØ
ā¬›šŸŸØā¬›šŸŸ©šŸŸ©
🟩🟩🟩🟩🟩

Wordle 67 4/6*

šŸŸØā¬›ā¬›ā¬›ā¬›
ā¬›šŸŸ©ā¬›šŸŸ©ā¬›
šŸŸ©šŸŸ©šŸŸ©šŸŸ©ā¬›
🟩🟩🟩🟩🟩

Wordle 1,336 6/6*

ā¬›ā¬›ā¬›šŸŸØā¬›
ā¬›ā¬›šŸŸØā¬›ā¬›
ā¬›šŸŸ©ā¬›ā¬›ā¬›
ā¬›šŸŸ©ā¬›ā¬›šŸŸØ
šŸŸ©šŸŸ©šŸŸ©šŸŸ©ā¬›
🟩🟩🟩🟩🟩

ā€ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€

Understanding Wordle Mechanics

Wordle color meanings:

  • 🟩 (green) = correct letter in correct position
  • 🟨 (yellow) = letter exists in word but wrong position
  • ⬛ (gray) = letter not in word

The key insight: the first row of each game represents the same starting word (as stated in the challenge). We need to reverse engineer this word from the patterns.

Finding Historical Wordle Answers

The Wordle number corresponds to days_since_launch. Using the NYT Wordle API (https://www.nytimes.com/svc/wordle/v2/YYYY-MM-DD.json), we can find the actual answers: ā€ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€

Wordle #DateAnswer
1June 20, 2021REBUT
67Aug 25, 2021CRASS
1336Feb 14, 2025DITTY

Constraint Analysis

Now we have three constraints for our unknown starting word:

  1. vs REBUT: ā¬›ā¬›šŸŸØā¬›šŸŸØ - positions 3 and 5 are yellow
  2. vs CRASS: šŸŸØā¬›ā¬›ā¬›ā¬› - position 1 is yellow
  3. vs DITTY: ā¬›ā¬›ā¬›šŸŸØā¬› - position 4 is yellow

Solution

Solver Script

python
#!/usr/bin/env python3
"""
Reverse Wordle Solver
Finds the starting word that matches all three Wordle patterns
"""
ā€ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€

def check_wordle_result(guess, answer):
    """Simulate Wordle's color-coding algorithm"""
    result = ['⬛'] * 5
    answer_chars = list(answer.lower())
    guess = guess.lower()
    
    # First pass: mark greens (correct position)
    for i in range(5):
        if guess[i] == answer_chars[i]:
            result[i] = '🟩'
            answer_chars[i] = None
    
    # Second pass: mark yellows (wrong position)
    for i in range(5):
        if result[i] == '⬛' and guess[i] in answer_chars:
            result[i] = '🟨'
            answer_chars[answer_chars.index(guess[i])] = None
    
    return ''.join(result)
ā€ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€


def main():
    # Target patterns (first row of each game)
    targets = [
        ('REBUT', 'ā¬›ā¬›šŸŸØā¬›šŸŸØ'),  # Wordle 1
        ('CRASS', 'šŸŸØā¬›ā¬›ā¬›ā¬›'),  # Wordle 67
        ('DITTY', 'ā¬›ā¬›ā¬›šŸŸØā¬›'),  # Wordle 1336
    ]
    
    # Load valid 5-letter words
    with open('wordle_words.txt', 'r') as f:
        words = [line.strip().upper() for line in f if len(line.strip()) == 5]
    
    # Find word matching all constraints
    for word in words:
        matches_all = True
        for answer, expected_pattern in targets:
            if check_wordle_result(word, answer) != expected_pattern:
                matches_all = False
                break
        
        if matches_all:
            print(f'Found starting word: {word}')
            return word
    
    print('No matching word found')
    return None
ā€ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€


if __name__ == '__main__':
    main()

Result

Running the solver finds: SQUIB

Verification:

  • SQUIB vs REBUT: ā¬›ā¬›šŸŸØā¬›šŸŸØ āœ“ (U yellow at pos3, B yellow at pos5)
  • SQUIB vs CRASS: šŸŸØā¬›ā¬›ā¬›ā¬› āœ“ (S yellow at pos1)
  • SQUIB vs DITTY: ā¬›ā¬›ā¬›šŸŸØā¬› āœ“ (I yellow at pos4)

Generate Flag

python
import hashlib

word = 'SQUIB'
sha256_hash = hashlib.sha256(word.encode()).hexdigest()
print(f'uoftctf{{{sha256_hash}}}')
# Output: uoftctf{REDACTED}

Tools & Resources

  • NYT Wordle API: https://www.nytimes.com/svc/wordle/v2/YYYY-MM-DD.json
  • Wordle word lists: Available from various GitHub repos
  • Python hashlib: For SHA256 hash generation ā€ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€

Lessons Learned

  1. Game share results often leak information that can be reversed
  2. Historical game data is often accessible via APIs
  3. Constraint satisfaction can narrow down possibilities quickly
  4. Always verify your solution against all given constraints ā€ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€‹ā€‹ā€Œā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€Œā€Œā€‹ā€‹ā€‹ā€‹ā€