Hunting Deserialization Vulnerabilities With Claude

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.

Luckily, the error message is quite explicit about the problem, and a small change to the Dockerfile will fix the problem.
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:
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.
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, 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:
{
"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.

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.

Next, we tell Claude to decompile and review System.AddIn.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.

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.

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

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

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

It also recognizes that the cache file is potentially 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.

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.

Claude also correctly identifies the 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.

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
# Create the AddIns.store file with the format expected by ReadCache<T> 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.

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.

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

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.
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.

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.

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.

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.

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.