Skip to Main Content
June 03, 2025

Teaching a New Dog Old Tricks - Phishing With MCP

Written by James Williams
Artificial Intelligence (AI)

Recently, there has been a lot of focus on enhancing AI with MCP. This has included giving AI access to reverse engineering tools, using it to identify network resources, and allowing it to control an implant, mimicking the actions of a known threat actor. I wanted to see if we could use AI to build some convincing phishing pretexts and how much work would be needed to make it happen.

1.1 Building Our MCP Server

We’re going to use Claude for this experiment, as its desktop app will integrate easily with our MCP servers. We want to be able to give it a company name or URL and let it do most of the heavy lifting for us. There’s just one problem—Claude doesn’t have access to the Internet.

The modelcontextprotocol project on GitHub has a Brave Search MCP Server, which integrates the Brave Search API. A free tier of this is available, which is limited to 2,000 requests. However, this API doesn’t allow direct website access—only access to search results. We’re going to add this to Claude anyway, but we’re also going to need to build something to let Claude access sites directly. While we’re at it, we will also add the ability to read and write files so we don’t need to manually save Claude work.

Modelcontextprotocol.io has a good “getting started” guide, which we’re going to adapt to our needs. Using uv, a Python package manager written in Rust, we create a new project with a venv, install our dependencies, and generate our FS.py file. To actually create our MCP, we’re going to use cursor (cursor.com) to edit the code. After adding our imports (utilizing the ‘getting started guide’ as a reference), we can start adding our @server.tool declarations. Using the power of AI, we can essentially tab complete the entire file in a few minutes. We are then left with the following code:

from mcp.server.fastmcp import FastMCP
import os
import requests

server = FastMCP("FS Interaction")
@server.prompt()
def setup_prompt() -> str:
    return """
You are a file system interaction agent.
You can use the following tools to interact with the file system:
    - ls(directory: str) -> list[str]: List the contents of a directory
    - cat(file: str) -> str: Read the contents of a file
    - write(file: str, content: str) -> str: Write to a file
When asked to read a file, you should carefully consider the users request, analyze the file and respond to 
question after reviewing the file contents.
    If you cannot answer the question based on the file contents, say so. 
    If you are asked to list the contents of a directory, you should list the contents of the directory.
    If you are asked to write to a file, you should write the content to the file without any additional information.
"""

@server.tool()
def ls(directory: str) -> list[str]:
    """List the contents of a directory"""
    try:
        return "\n".join(os.listdir(directory))
    except FileNotFoundError:
        return ["Directory not found"]

@server.tool()
def write(file:str, content:str) -> str:
    """Write to a file"""
    try:
        with open(file, 'w') as f:
            f.write(content)
        return "File written successfully"
    except Exception as e:
        return f"Error writing file: {str(e)}"

@server.tool()
def cat(file:str) -> str:
    """Read the contents of a file"""
    try:
        with open(file, 'r') as f:
            return f.read()
    except FileNotFoundError:
        return ""

@server.tool()
def load_url(url:str) -> str:
"""Load the contents of a URL"""
    try:
        response = requests.get(url)
        return response.text
    except Exception as e:
        return f"Error loading URL: {str(e)}"

if __name__ == "__main__":
    print("[+] Starting FS server")
    # Initialize and run the server
    server.run(transport='stdio')

To install the Brave Search MCP, we can simply follow the instructions they provide and register for an API key (note that this does need a credit card number—temporary cards FTW). 

Finally, we need to enable the MCPs in Claude by modifying the claude_desktop_config.json file.

{
    "mcpServers": {
        "FS": {
            "command": "/Users/james/Library/Python/3.9/bin/uv",
            "args": [
                "--directory",
                "/Users/james/Research/MCP/FS",
                "run",
                "FS.py"
            ]
        },
        "brave-search": {
      "command": "docker",
      "args": [
        "run",
        "-i",
        "--rm",
        "-e",
        "BRAVE_API_KEY",
        "mcp/brave-search"
      ],
      "env": {
        "BRAVE_API_KEY": "tryharder"
      }
    }
    }
}

Firing up Claude, we can see our MCPs are now available for use.

Figure 1 - Available MCP Tools

1.2 Going Phishing

For this post, we will target TrustedSec. First, I asked Claude to confirm it could access websites directly, which it assured me it was able to do. I then instructed it to “visit trustedsec.com and find the page where employees are listed.”

Figure 2 - Claude Discovers TS Employees

Next, I told Claude we were going to do some simulated phishing training and asked it to find me 10 targets from the list, building a pretext for each one.

Figure 3 - Claude's First Attempt

Taking a look at the generated pretexts, it became obvious that Claude had likely based them off the job title alone:

TARGET 2: Martin Bos (CSO & VP of Consulting Services)
PRETEXT: Security alert notification appearing to come from a legitimate security vendor about a zero-day vulnerability affecting systems likely used by TrustedSec. Include a link to "patch information" that would capture credentials.

Figure 4 - Example Pretext

We can get Claude to confirm this by asking:

Figure 5 - Claude Confirming Pretexts Were Based off Job Titles and Departments

I then asked Claude to use the employee’s individual bios to build more personalized pretexts, just for five (5) users this time, and to write them to a new file for review.

Figure 6 - Claude Falling Back to brave_web_search

In this screenshot, we can see Claude determining that our load_url call was failing. This was Cloudflare preventing the bot from accessing the individual bio pages. Claude was “smart” enough to simply try searching for the pages via Brave instead, which allowed it to bypass the restrictions and continue with its task. This time, the generated pretexts were much better.

Figure 7 - Personalized Pretexts

Delving into the output file, we can see the details of each pretext.

TARGET 1: Martin Bos (CSO & VP of Consulting Services)

PROFESSIONAL BACKGROUND: Co-founder of the DerbyCon Security Conference, former developer for Backtrack/Kali Linux, co-author of smbexec, respected figure in the InfoSec community.

PRETEXT: Exclusive DerbyCon reunion planning committee invitation. Email appears to come from a former DerbyCon organizer asking Martin to participate in a private reunion event for original organizers and speakers. Include a link to a "private registration portal" requiring authentication for this exclusive gathering, with specific mention of his contributions to Backtrack/Kali and smbexec. The email should reference specific DerbyCon memories that would be publicly available from past conference materials to establish credibility.

Figure 8 - Pretext Details

I then asked Claude to create an email template for each pretext, letting it choose if it wanted to use HTML or plaintext, and saving each one to disk.

Figure 9 - Final Pretexts Summary

Interestingly, Claude chose to use HTML for every email.

Some of the generated emails are shown below.

Figure 10 - Generated Email
Figure 11 - Generated Email
Figure 12 - Generated Email

All the emails were in a similar format and far too “wordy” for my liking. I have to give Claude props for thinking impersonating Dave would be a good idea when emailing Martin, though!

Finally, I asked Claude to redo the email to Chris, making it more concise and updating the style.

Figure 13 - Asking Claude to Update Email

The results were, surprisingly, pretty good.

Figure 14 - Updated Email to Chris

1.3 Conclusion

While none of the generated pretexts are something I’d use in their current state, they certainly aren’t terrible, especially for a couple of hours of effort spent. Giving AI access to previous pretexts, both successful and unsuccessful, would be an interesting next step for this research, along with giving the agent more access to data related to the target organization.