I saw this post about a Golf Gang Workshop map exploit after a player reported losing Roblox items worth around $10,000. The value surprised me, but I wanted to know how a map could lead to that. A few months earlier I’d looked at a Workshop issue in Meccha Chameleon, so I suspected another content loader problem. That sent me into the Golf Gang course format, then into other Steam games.

I executed arbitrary code through player content in four games: a Golf Gang course, a Tank Team map, a Goat Simulator mod and a Puck plugin. Their loaders crossed different trust boundaries.
I also wrote about code execution through a Steam Workshop crosshair in Crosshair X and recreating a popup inside Steam.
Golf Gang: the visual-script filter stops at SuperUnit
Golf Gang’s .ggcourse loader accepts Bolt flow graphs. WorkshopCourse.PostProcessCourse calls FlowMacroIsSafe, which checks direct GetMember, SetMember, InvokeMember and Literal elements against blocked type names such as System.Diagnostics. It does not inspect the graph inside a SuperUnit.
The check’s relevant logic, expressed as pseudocode from the installed assembly:
for each element in the supplied FlowMacro:
if element is GetMember, SetMember, InvokeMember or Literal:
check its referenced type against blocked names
if element is SuperUnit:
child FlowMacro is not checked
The course’s serialized graph:
FlowMachineSerializer → root FlowMacro
Start → SuperUnit → child FlowMacro
InvokeMember: System.Diagnostics.Process.Start("calc.exe")
The root passes the check. When Hole 1 starts, Bolt follows the unchecked child and opens Calculator. No DLL is needed. I tested the course through the game’s local Workshop-format loader.
Tank Team: Map.sav reached BinaryFormatter.Deserialize
In Tank Team build 20239328, opening Maps → Custom parsed subscribed Workshop maps. The call chain traced in the installed assembly:
MapList.ShowWorkshopCR
→ Steam.GetSubscribedMap(itemId)
→ Save.LoadSteam(folder, "Map")
→ BinaryFormatter.Deserialize(Map.sav)
→ parse the returned map string
Save.LoadSteam deserialized the entire file before casting its root to a map string. The ordering is the flaw (pseudocode):
var map = (string)new BinaryFormatter().Deserialize(mapStream);
return ParseMapRows(map);
My Map.sav kept a valid map string as its root and placed the process-launching records inside the formatter stream. Opening the Custom list displayed the map card and started two Calculator processes. The trigger was list loading, before any map click. I tested the local installed copy of my private subscribed item.
The later build 25885196 uses Save.LoadLevel and JsonUtility.FromJson<Level> for this Workshop path; the BinaryFormatter result applies to the earlier build.
Goat Simulator: GoatMod.ini registers executable UnrealScript
My CalcPoC mod packages a compiled UnrealScript mutator. Its GoatMod.ini registers the class:
[GoatGame.GGDownloadableContentManager]
+mAllMutators=(FileName="CalcPoC.GGMutatorCalcPoC",DisplayName="Calc PoC",Description="Benign local Workshop execution proof-of-concept")
The mutator’s InitMutator calls the engine’s launcher:
class'Engine'.static.LaunchURL("C:/Windows/System32/calc.exe");
GoatMod.ini selects the compiled mutator; InitMutator then calls LaunchURL. The mod system runs UnrealScript by design. The entry point is the registered mutator, not a map parser.
Watch my Goat Simulator PoC on YouTube:
Puck: the plugin DLL runs on enable
Puck scans Plugins for DLLs, loads them with Assembly.LoadFile, finds IPuckPlugin implementations and calls OnEnable. My plugin source:
public sealed class CalcProbe : IPuckPlugin
{
public bool OnEnable()
{
try
{
Process.Start("calc.exe");
return true;
}
catch (Exception)
{
return false;
}
}
}
With the DLL in Plugins, launching Puck opened Calculator. Puck.log recorded Adding plugin CalcProbe. This is intended DLL plugin execution; I did not test a Workshop arena.
Golf Gang and the older Tank Team build turned course or map data into execution. Goat Simulator and Puck accept executable mods by design. The distinction is in the loader: graph interpreter, object deserializer, script runtime or assembly loader.