8 minute read

June 12, 2025

Hunting Deserialization Vulnerabilities With Claude

Written by James Williams

Artificial Intelligence (AI)

Table of contents

Share

In this post, we are going to look at how we can find zero-days in .NET assemblies using Model Context Protocol (MCP).

Setup

Before we can start vibe hacking, we need an MCP that will allow Claude to disassemble .NET assemblies. Reversing a .NET binary is normally something I would do with dotPEAK; however, this is a Windows-only tool. Luckily for us, ilspycmd exists and can be run on Mac/Linux. The ilspycmd-docker repository provides a Dockerfile for ilspycmd, but the current version on GitHub is a few years out of date and won’t build.

Figure 1 - Build Error for ilspycmd-docker

Luckily, the error message is quite explicit about the problem, and a small change to the Dockerfile will fix the problem.

```hljs bash FROM mcr.microsoft.com/dotnet/sdk:8.0

RUN useradd -m -s /bin/bash ilspy USER ilspy

WORKDIR /home/ilspy

RUN dotnet tool install -g ilspycmd

RUN echo ‘export PATH=”$PATH:/home/ilspy/.dotnet/tools/”’ » /home/ilspy/.bashrc

ENTRYPOINT [ “/bin/bash”, “-l”, “-c” ]


We can build this new image with the following command:

```hljs undefined
docker build -t ilspycmd .

With our Dockerfile updated and our container built, we can build a simple MCP server using Python. We’ll use the same framework as shown in our previous blog that discusses building an MCP server.

```hljs python from mcp.server.fastmcp import FastMCP import subprocess import os

server = FastMCP(“ilspy docker”)

@server.prompt() def setup_prompt() -> str: return “”” You can use the following commands to decompile .NET assemblies, using ilspy: - decompile(file: str, output_folder: str) -> int: Decompile the file at the provided path. The returned value is the success code, with 0 indicating a successful run “””

@server.tool() def run_ilspycmd_docker(exe_path, output_folder) -> int: “”” Run ilspycmd in a Docker container to decompile a DLL

Args:
    dll_path (str): Path to the DLL file to decompile
    output_folder (str): Folder where decompiled code will be placed

Returns:
    tuple: (return_code, stdout, stderr)
"""
# Get absolute paths
input_dir = os.path.abspath(os.path.dirname(exe_path))
output_dir = os.path.abspath(output_folder)
exe_filename = os.path.basename(exe_path)

# Ensure output directory exists
os.makedirs(output_dir, exist_ok=True)

# Create input directory inside container
container_input_dir = "/decompile_in"
container_output_dir = "/decompile_out"

ilspy_cmd_path = "/home/ilspy/.dotnet/tools/ilspycmd"
ilspy_command = f"{ilspy_cmd_path} -p -o {container_output_dir} {container_input_dir}/{exe_filename}"

# Build the Docker command
docker_cmd = [\
    "docker", "run", "--rm",\
    "-v", f"{input_dir}:{container_input_dir}",\
    "-v", f"{output_dir}:{container_output_dir}",\
    "ilspycmd",\
    ilspy_command\
]

# Run the command
process = subprocess.run(
    docker_cmd,
    stdout=subprocess.PIPE,
    stderr=subprocess.PIPE,
    text=True
)
return process.returncode

if name == “main”: # Initialize and run the server server.run(transport=’stdio’)


This is a little more complicated than the example in our [previous blog](https://trustedsec.com/blog/teaching-a-new-dog-old-tricks-phishing-with-mcp), but at a higher level, we’ll use some file paths to run a Docker command. Next, we’ll edit the **_claude\_desktop\_config.json_** file and add our new MCP server. It will look something like this:

```hljs json
{
    "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": "try_harder"
      }
    },
    "ilspy": {
            "command": "/Users/james/Library/Python/3.9/bin/uv",
            "args": [\
                "--directory",\
                "/Users/james/Research/MCP/ilspy_docker",\
                "run",\
                "ilspy.py"\
            ]
        }
    }
}

After restarting Claude for Desktop, we should see that our MCP server is now available.

Figure 2 - MCP Server Available

Finding Existing Vulnerabilities

Now, let’s see if Claude can find a known vulnerability. In September 2023, Blue-Prints Blog posted about an insecure deserialization in AddinUtil.exe, a .NET binary that ships with Windows by default. This binary has also been added to the LOLBAS project. Note that if you want to follow along at home, you’ll need the MCP servers we created in this blog.

This vulnerability is interesting because the unsafe deserialization happens in a DLL, but the entry point is in an .EXE. First, we’ll see if Claude can find the vulnerable code in the DLL. Let’s start by checking if Claude recognizes the new MCP server.

Figure 3 - Confirming Claude can Decompile .NET Binaries

Next, we tell Claude to decompile and review System.AddIn.dll.

Figure 4 - Telling Claude to Review the DLL

Eventually, Claude comes back with a list of potential vulnerabilities, including an unsafe deserialization call in .NET remoting due to the use of TypeFilterLevel.Full.

Figure 5 - Deserialization Identified in .NET Remoting

This is likely related to the functionality used by AddinProcess.exe, which contains a .NET remoting vulnerability identified by Nettitude.

We want to focus on deserialization flaws, so we adjust our prompt and try again.

Figure 6 - Adjusted Prompt

This also failed to find the known vulnerability. Now, let’s Claude ask which files have been reviewed.

Figure 7 - Claude Listing 14 Reviewed Files

Luckily, Claude will usually tell us the truth when asked, so let’s ask why it has only reviewed 14 files.

Figure 8 - Claude’s Confession

After confirming that we want it to review all of the files, Claude finally identifies the known vulnerability.

Figure 9 - Claude Identifies the Unsafe Deserialize Call

It also recognizes that the cache file is potentially untrusted.

Figure 10 - Claude Recognizing the Cache File is Untrusted

So far, so good. Now we want to see if Claude is effective enough to find the actual exploit path from AddinUtil.exe to the unsafe deserialize call.

We’re going to give Claude a little guidance here and tell it that the DLL is referenced by AddinUtil.exe. We could get Claude to figure this out by itself, but that will be the subject of a future post.

Figure 11 - Telling Claude to Identify Paths to the Unsafe Code

After a little thinking, Claude successfully identifies the possible entry points, including the pipelineroot flag, which is mentioned in the Blue-Prints Blog post as another path to a deserialize call.

Figure 12 - Claude Identifies Entry Points

Claude also correctly identifies the attack path.

Figure 13 - Claude Identifies Attack Path

Time to push our luck and see if Claude can figure out how to build an exploit for this vulnerability. For those who didn’t read the Blue-Prints Blog, AddinUtil.exe expects the cache file to contain a specific series of bytes which precede the data that is deserialized. Let’s see if Claude can figure this out.

Figure 14 - Prompting Claude to Build a Proof-of-Concept Tool

The generated code, which I won’t reproduce in full here, references the fact that we need to create the store file in a format expected by ReadCache.

```hljs lua

Create the AddIns.store file with the format expected by ReadCache print("[+] Creating malicious AddIns.store file")

cache_file_path = os.path.join(output_dir, “AddIns.store”)

with open(cache_file_path, ‘wb’) as f: # Write the format version (int32 = 1) f.write(struct.pack(“<i”, 1)) # Write the payload size (int64) f.write(struct.pack(“<q”, len(payload_data))) Write the payload data f.write(payload_data)


We can get Claude to explain its choice here, just to see if it understands the exploit code.

![](https://trusted-sec.transforms.svdcdn.com/production/images/Blog-assets/HuntingDeserialVulnsClaude_Williams/Fig15_Williams_VulnsWithClaude.png?w=320&q=90&auto=format&fit=max&dm=1749527503&s=d7616bdec1544cf5550ff3787e2898bd)Figure 15 - Claude Correctly Identifying the Padding Needed

So, it correctly recognizes that we need to pad by 12 bytes but guesses at the purpose of those bytes. They are not used in the decompiled code, so it is impossible to determine their actual purpose.

## Finding New Vulnerabilities

Let’s see if Claude can identify the attack path for the **_pipelineroot_** flag, which wasn’t expanded on in the Blue-Prints Blog.

![](https://trusted-sec.transforms.svdcdn.com/production/images/Blog-assets/HuntingDeserialVulnsClaude_Williams/Fig16_Williams_VulnsWithClaude.png?w=320&q=90&auto=format&fit=max&dm=1749527504&s=282644f1d57cecbb31b8b6087ff04725)Figure 16 - Prompting Claude to Generate a Proof of Concept for the pipelineroot Flag

After generating some Python code (included at end of this post), Claude gives a detailed explanation of the vulnerability.

![](https://trusted-sec.transforms.svdcdn.com/production/images/Blog-assets/HuntingDeserialVulnsClaude_Williams/Fig17_Williams_VulnsWithClaude.png?w=320&q=90&auto=format&fit=max&dm=1749527505&s=e930e80573990c8f0602926a69414377)Figure 17 - Claude Explaining the pipelineroot Path

All that’s left to do is check Claude's work. We’ll use a simple **_ysoserial.net_** payload to pop **_calc.exe_** as our Proof-of-Concept payload, which we’ll use with the code generated by Claude.

```hljs python
ysoserial.exe -f BinaryFormatter -g TypeConfuseDelegate -c calc -o raw > e:\tools\payload.bin

We can then run the Proof-of-Concept code generated by Claude.

Figure 18 - Running the Proof-of-Concept Code

Running AddinUtil.exe with the generated -pipelineroot flag, perhaps unsurprisingly, didn’t work. Luckily, we can debug this quite easily with Visual Studio and dotPeek. First, we make sure dotPeek has the .EXE and DLL loaded, then we start its symbol server. Next, we decompile AddinUtil.exe using dotPeek and create a project file. We load this file into Visual Studio, then add dotPeek as a symbol server. Finally, we change the project debug options to start the compiled .EXE, so we can pass our pipelineroot flag as an argument.

Figure 19 - Visual Studio Debug Options

Now, we can add a break point to the decompiled code, run the binary, and enjoy step-through debugging.

After a quick step-through, it becomes apparent that Claude had missed a step.

Figure 20 - AddIns.store File Check

Claude has neglected to create the AddIns directory and named the .store file something else, meaning this check failed and BuildAddInCache was never called. A quick update to the generated Python code resulted in a folder structure that passed all the checks and, when executed, popped calc.exe.

Figure 21 - calc.exe Launched

This exploit isn’t as useful as the -AddinRoot flag because we need to drop a complete folder structure to disk. I haven’t seen a public implementation for this attack path yet, which makes it a good candidate for testing with Claude.

Final Thoughts

In this post, we’ve seen how we can use an MCP server to give Claude the ability to analyze .NET assemblies. We’ve used that ability to find a known vulnerability in a Microsoft-signed binary and seen how we need to be explicit when giving Claude instructions (such as telling it to review every file). Finally, we’ve built a working Proof-of-Concept for an attack path mentioned in the original disclosure of this vulnerability, which was left as an exercise for the reader in the Blue-Prints blog.

While we had to give Claude a few hints along the way, this process of analyzing a file and getting close to a working exploit was much faster than what we could do manually. The next step will be to see if it’s possible to do this at scale, but that will be the subject of a future post.

The full Proof-of-Concept code can be found here.

CloseShow Transcript

Updated: