Two-click RCE in Meccha Chameleon...again
Introduction
Meccha Chameleon is a game that has blown up recently for its unique gameplay. Following in the footsteps of similar games like PEAK, Lethal Company, and Among Us, you and 2-10 of your closest friends come together to play prop hunt against each other while painting yourself to blend in with the environment around you. See if you can spot my friend hiding in this picture:

Custom Maps
A core part of the game is downloading community maps from the steam workshop. When loading into a public lobby, the host can pick from a variety of maps, including custom ones. If the host picks a map from the workshop that you’re not subscribed to, a prompt appears to get you to install the map. When a player completes the flow and hits Subscribe+ on the workshop page, the game loads the map into memory.

This flow has been highly normalized within the game’s community, and millions of players trust it every day without a second thought. At the time of writing, the two most-subscribed maps have 11 million subscribers between them. The potential for abuse here seemed pretty high, so I decided this would be my main target for analysis.

Prior research
While doing my initial research, I ran into Khael Kugler’s writeup about a previous vulnerability in the game. His chain bundled content into the map’s steam workshop upload and abused Unreal Engine’s Blueprint system to launch an executable .bat script at an attacker-controlled path on the victim machine.
The original chain looked like this:
- The attacker builds a map with embedded blueprints that call
LaunchURLwith thefile:///schema at a known location. Since steam workshop files get predictable paths, the attacker points at that path. - The attacker uploads a map to the community workshop with an embedded
.exe,.bat, or other payload. - The attacker player then hosts the map in-game and a victim player downloads the map.
- After subscribing and downloading the map, the script executes on the target player’s machine.
The original bug
Unreal Engine has a visual scripting language called Blueprints that game developers can use in lieu of spending time doing things manually with C++. Developers can use them to do lots of things like influencing players, changing lighting, and just about anything else you can do in code. The most notable function for our investigation is LaunchURL.
The original bug worked because the LaunchURL function eventually calls ShellExecuteW(NULL, "open", URL, NULL, NULL, SW_SHOWNORMAL). The open verb in this Windows API function means that when a file is passed with it, Windows uses the same logic to handle it as when the file is double-clicked by the user. Since this is default Unreal Engine behavior, the devs patched the delivery mechanism instead of fixing LaunchURL. If you heard about the Windows Notepad RCE this year, you may recognize this as the same class of bug.
Patch analysis: the workshop cleaner
The devs reportedly patched the original bug, so I began to dig into the controls they put in place. The patch included a cleaning function, and the logic looked like this:
static const wchar_t* ALLOW[5] = { L".png", L".pak", L".ucas", L".utoc", L".bin" };
// Batch cleaner: for each flagged item, delete disallowed files from its workshop dir.
// Non-recursive scan; content-blind extension check; fails closed on delete error.
bool clean_workshop_items(const Item* items, size_t n, List* deleted, Error* err) {
for (size_t i = 0; i < n; i++) {
if (!items[i].needs_clean) continue;
wchar_t path[MAX_PATH];
if (!SteamUGC_GetItemInstallInfo(items[i].id, path, MAX_PATH)) continue;
if (!DirectoryExists(path)) {
format(err, L"Workshop install folder does not exist: %s", path);
format(err, L"Failed to clean Workshop item %s before mount. %s", items[i].name, err);
return false;
}
FileEntry* files; size_t nf = FindFilesFlat(path, L"*", &files); // NON-recursive; .name is a full path
for (size_t f = 0; f < nf; f++) {
const wchar_t* ext = wcsrchr(files[f].name, L'.'); // last '.'
if (!ext) ext = L"";
bool ok = false;
for (int a = 0; a < 5; a++)
if (_wcsicmp(ext, ALLOW[a]) == 0) { ok = true; break; }
if (ok) continue;
if (!DeleteFileEvenReadOnly(files[f].name)) { // fails closed
format(err, L"Failed to delete disallowed Workshop file before mount: %s", files[f].name);
format(err, L"Failed to clean Workshop item %s before mount. %s", items[i].name, err);
return false;
}
append(deleted, files[f].name);
}
}
return true;
}
Upon mounting the workshop directory to load the map, it walks the folders and deletes any files that don’t match the .png, .pak, .ucas, .utoc, or .bin extensions. Digging into the disassembly further, I realized that there were still no restrictions on LaunchURL.
Testing the patch
After many hours of troubleshooting, setting up Unreal Engine, and getting the sample map loaded, I was ready to get to work. I wanted to test if I could embed a blueprint to launch a pre-staged script on my computer. If successful, this would prove that the RCE vector was still viable. My working POC looked like this:

I went through the process of compiling and uploading my map to the community workshop, and then loaded the map in a private game. Upon loading the map, my script executed!

Continuing my testing, I unsuccessfully attempted to bypass the cleaner function. The code execution primitive was intact, but I didn’t have a way to get my payload to disk.
Looking for a write primitive
The only thing stopping me was not having control over a path on disk, so I started looking for different blueprint functions to do that. I examined a few options, but ultimately landed on the Save to File function. This function can be used to write the raw bytes of an Interchange Base Node Container to disk, with control over the last few bytes. Here’s what my initial payload looked like:

I ran the blueprint and it successfully wrote my payload to disk, but failed to execute. After doing some digging, I discovered that the output of the function was in UTF16-LE, which my batch file choked on. I gave up on this route for the time being, and pivoted to another path.

Pivoting to external URLs
Since my disk write was effectively dead, I started looking for a way to pull and launch remote payloads, and eventually got around to this flow:

This flow abuses LaunchURL both to download the payload and execute it. The blueprint flow constructs a filepath by resolving the running user’s home directory, and then concatenates that together with the rest of the payload path in Downloads. The flow went Game loads map -> LaunchURL uses the browser to download a payload -> Game freezes, prompts the user for untrusted payload launch -> player hits "Run" -> RCE.
The catch with this method is that any payload downloaded like this is hit by Microsoft’s Mark-of-the-Web. This prevents the payload from running silently, and makes the external download method effectively a three-click RCE method, which can be seen in this video:
With my issue documented, I jumped through Discord servers, reached out to the devs on Twitter, and waited for a response.
Two weeks later…(July 25th)
A couple of weeks after reaching out to the devs and taking a break from the project, I got a DM about an active malware campaign that was using malicious Meccha maps to spread to players. The full breakdown by Feint is worth a read.
TLDR: The malware creator found a write primitive.

The malware used a JSON/BAT file polyglot with Epic’s Save Json to File plugin to write a file that was both a valid .bat and .json file.

The same day it blew up on X, the devs patched the bug, and put out these statements:

The fix
For now, the devs have addressed the root cause of the issue. LaunchURL now only executes allow-listed web addresses in the browser. The devs also removed the ability to use the JSON plugin in-game.
Disclosure timeline (approximate):
- ~Jul 6-8: dug into the workshop surface, set up UE, analyzed the directory-cleaner patch
- ~Jul 12: confirmed
LaunchURLstill reachedShellExecuteW; calc PoC; bundled-file path confirmed dead; pivoted to external-URL RCE (three-click RCE confirmed), reported issue to developers - ~Jul 25: Malware campaign gained visibility in the community, developers patched
LaunchURL.
Credit to Khael Kugler for the original research, Feint for the malware-campaign breakdown, and the Meccha Chameleon team for shipping the fix in such a short amount of time.