← Terug naar blog
|

Pwning Call of Duty 1: a 20-year-old RCE, found in an evening with AI

ai pentesting red-team reverse-engineering exploit-development call-of-duty

Part 1 of 3 in Pentesting with AI.

I still have a soft spot for Call of Duty 1. It came out in 2003, and I've played it for many hours and many years. At that time I learned a lot, from managing Linux for hosting game servers to modding the game. I always wanted to build a security CTF around this game. So started to play again with the game a few weeks ago.

One evening I pointed an AI coding agent at the old Linux dedicated server binary, cod_lnxded, and asked a simple question: is there something in here that lets me run code on the box?

There was. And what surprised me was not that the bug existed, it is a 2003 game server, of course it has bugs, but how little of the evening it actually took to find and exploit the bug. In 2026, it takes a evening: just let the AI hunt for bugs.

In this serie I want to show how unbelievable powerful AI is as a tool in penetration testing. It's important for us as pentesters to adopt this, and also for our customers to allow us to use it.

What is cod_lnxded?

cod_lnxded is the Linux dedicated-server build of Call of Duty 1. You run it, it hosts a match, players connect over UDP. Admins control it through rcon, a remote console protocol where an operator with the rcon password can send server commands, like changing the map.

It is a 32-bit ELF. And when you actually look at how it was compiled, no PIE, no stack canary, no RELRO, and an executable stack. Every single thing that would normally get in your way is simply switched off. Not really a suprise, because its from the year 2003.

The setup

This is the part I want you to remember across this whole series, because it is the same every time. And very important to hunt bugs with AI effectivly.

I ran the game server in a disposable Docker container, so I had a live target the AI could crash and restart as often as it liked. I gave Claude Code access to that container and to the binary, and let it drive the tooling, objdump, readelf, and a few small scripts it wrote itself to scan the disassembly. No magic, just a very fast, very patient reverse engineer that never gets bored reading assembly. Claude was surprisingly good in reversing ELF binaries.. (way faster & better then me)

A live target the AI can break + the ability to read the target source + an AI agent to do a lot of work in a short time. That combination is the whole story. It's all you need.

Reversing a 20-year-old binary

I asked the agent to find vulnerabilities in public available inputs that result in critical vulnerabilities. First it focussed on user functionalities, later it switched to admin functionalities (rcon). It walked into SV_Map_f, which takes the map-name argument and hands it to a normalizer function before validating anything. That function does roughly this:

char buf[72];
memcpy(buf, name, strlen(name));   // no bound check

A 72-byte stack buffer, and a memcpy sized by the length of the attacker-controlled name. The saved return address sits at buf+76. So a map name of 76 bytes plus an address overwrites where the function returns to. Classic stack smash, reachable over the network through the map command.

I want to be honest about the reach here, because I don't want to make this sound bigger than it is: this is reached through rcon, so it needs operator access. It is a post-authentication RCE, not an unauthenticated one. What makes it interesting is not the auth boundary, it's how super fast AI can reverse engineer Linux binaries AND write working exploits in just an evening.

AI finds its way

Overwriting the return address is easy. Landing somewhere useful is where the evening got interesting.

Before the map name reaches that buffer, it goes through MSG_ReadString, which quietly rewrites bytes on the way in. Some bytes get swapped, anything 0x80 or above gets turned into a dot, and a null byte just terminates the string. That is a problem: every address I actually wanted, libc, the stack, is full of high bytes like 0xff and 0xf7. None of them survive the filter.

The .text section, though, lives down around 0x08…. Those bytes pass cleanly. So the plan became: point the saved return address at a jmp esp gadget already sitting in .text, land on the executable stack, and put the shellcode there.

And the shellcode had its own twist. The normal way to make a syscall on 32-bit x86 is int 0x80, but 0x80 is exactly one of the bytes the filter destroys. So the agent built alphanumeric-safe shellcode that sets up execve("/bin/sh", …) and fires it with sysenter instead. 😅

None of these individual tricks are new. Watching an agent chain them together against a real binary in one sitting is what felt new. The agent was doing all the work, I was holding my beer and enjoying it.

It runs

The last step is always the honest one: does it actually work end to end? It works. Fired against the live container, the exploit dropped a shell in the server process. That is code execution inside the actual Call of Duty 1 game-server process. This screenshot shows the id output coming back from the server user, after executing the exploit:

Screenshot of the exploit landing a shell in the Call of Duty 1 server process, with `id` returning uid=999(codsvr)

This is why the live container is powerful. The AI will try the exploit, if it fails it will fix it. Until the exploit works. No interaction of me is required at all.

Takeaways

A few things I took away from the evening:

  • The manual version of this, disassembling, tracing the map handler, working around a byte filter, hand-rolling sysenter shellcode, is a weekend (or more) of work for most people. With an agent it was an evening. Also: people don't WANT to spend time on these kind of binaries because time is money. But now.. we have AI that can work around the clock.
  • A live container, the AI could crash freely and a binary the agent could read closely is what made it fast. Take either away and you are back to guessing. Your setup is the KEY!
  • I still made the calls, decided the strategy, and checked its work, it got plenty wrong along the way. But it turned hours of work into minutes, and that changes what one person can cover in an evening. The AI is able to process WAY more code then I can in a short period of time. It's like an vulnerability scanner +++

That last point is really what this series is about. In the next post I turn the exact same setup, live container, source, Claude Code, against something far more modern: our own AI-driven SOC pipeline. Thanks for reading!