A site owner asked whether a file from their commercial WordPress theme was a backdoor. They believed it was a legitimate theme file. Both readings had a point: the file looked exactly like malware. It was a single PHP class wrapped in a bytecode virtual machine, with encrypted strings, anti-debugging traps, file integrity checks, a $_POST key handler and code that can kill the admin panel with a 403.
This article goes the other way from my usual write-ups. It does not show how malware hides. It shows how to prove that a frightening-looking file is not malware, without guessing and without trusting either the vendor or first impressions.
🚨 The symptom: "is this theme file a backdoor?"
The question is familiar to anyone who supports WordPress sites:
- A theme file such as
wp-content/themes/<theme>/inc/classes/<tools>.class.phpturns up in a review, and it is completely unreadable. - Removing it would break the theme's admin page.
- The owner says: "I bought this theme. That file came with it."
Two answers are always tempting and both are lazy. "It's obfuscated, so it's malware" and "It came with the theme, so it's safe" are both wrong as verdicts. Commercial themes get nulled and backdoored all the time, and commercial themes also ship legitimately encrypted license code. The only way to know is to look at what the code can do.
🎭 Why the file looks like a backdoor
Opening the file shows almost every red flag a malware analyst is trained to react to:
<?php
// Shape of the file (illustrative, not the original bytes)
$_x = gzinflate(base64_decode('7b0HYBxJliUmL23K...')); // ~130 KB of blobs
function _vm($op, $a, $b) { /* bytecode interpreter */ }
class TOOLS {
function urlRoot() { return _vm(0x1f, __CLASS__, __FUNCTION__); }
function isNormal() { return _vm(0x2a, __CLASS__, __FUNCTION__); }
// ...every method body is an opaque call into the VM
}
Under the first layer you find more reasons to worry:
- A custom VM. The methods are not PHP. They are bytecode for an interpreter that lives in the same file.
- Encrypted string pool. Every function name and every string is AES-encrypted and decrypted at runtime, so grep finds nothing.
- Anti-debugging. It checks for debuggers, debug extensions and timing gaps between instructions, and refuses to run if it finds them.
- Anti-tampering. The method decryption key is derived from the file's own source code. Change one byte of the interpreter and every method decrypts to garbage.
evalopcodes. The VM has an instruction that evaluates code.
Anyone matching on patterns sees all of this and reasonably calls it a backdoor. The question is whether that is a correct call or just a correct-looking one.
🔍 Rule one: judge capability, not appearance
Obfuscation tells you someone wanted the code hidden. Attackers want that, and so do vendors protecting a paid product. What separates them is what the code does once it runs. A backdoor needs at least one of these capabilities:
- Run attacker-supplied code:
eval,assert,include,create_functionorpreg_replace /eon data that comes from the request, the database or the network. - Fetch and run remote code:
curl,file_get_contents('http…'),wp_remote_get,fsockopenor streams feeding into an execution sink. - Run system commands:
system,exec,shell_exec,passthru,proc_openor backticks. - Grant access:
wp_create_user,wp_insert_user,set_role, edits towp_capabilitiesorwp_set_auth_cookie. - Write executable files:
file_put_contentsorfwritewith a.phptarget or a path taken from the request. - Exfiltrate data: sending request data, cookies or credentials to an outside host.
If none of these exist anywhere in the decoded code, the file cannot act as a backdoor, however ugly it looks. To check that, though, you have to decode it. Grepping the protected file proves nothing, because the function names are encrypted.
🧅 Peeling the layers (safely)
The work happens offline on a copy, never on the live server and never by executing the file. The anti-debug code exists precisely to punish people who run it under a debugger, and if it were malware, running it would be the worst possible move.
Step 1: inflate the outer blobs statically. The first layer is ordinary compressed string literals. Extract them and inflate them with a small script, not with PHP:
# Read-only: list the large literals in the file and their sizes
grep -oE "'[A-Za-z0-9+/=]{200,}'" sample.php.txt | awk '{ print length($0) }' | sort -n | tail
# Inflate one literal with Python (raw deflate, no PHP execution)
python3 -I -c "import sys,zlib,base64; print(zlib.decompress(base64.b64decode(sys.argv[1]), -15)[:400])" 'PASTE_LITERAL'
Step 2: read the interpreter. One inflated blob is the VM itself. Make it readable and map the opcode table: which number means "call a function", which means "push an argument", which means "eval". This is slow but simple work, much like reversing a tiny CPU.
Step 3: decrypt the string pool. The interpreter contains its own decryption routine for the constants. Reimplement it outside PHP and you get every function name and string literal the class uses.
Step 4: decrypt the method bodies. Here the anti-tamper layer matters. The key comes from the interpreter's source text and the IV from each method's own source text, so you have to feed your decryptor the exact original bytes and line ranges. Normalise whitespace or line endings and nothing decrypts. In our case 23 of 24 methods came out cleanly. The last one failed on a line-range off-by-one, which is a reminder to note gaps honestly instead of papering over them.
Step 5: disassemble. Run every decrypted body through your opcode map and print each function call with the decrypted strings resolved. Now you can grep, because you are searching real behaviour and not protected bytes.
I am deliberately not publishing the key derivation or a decoder for this specific protector. The method matters here. A ready-made tool would mostly help people who strip licensing from paid themes, and nulled themes are one of the most common ways backdoors reach WordPress sites.
🧾 What was actually inside
With the code readable, the class turned out to be a toolbox for the theme:
- Reading cache statistics from Memcached, Redis and OPcache for a theme admin panel.
- Reading server load from
/proc/loadavg. - Building screenshot URLs on the vendor's own screenshot service.
- Getting the visitor's IP and doing a DNS lookup over UDP.
- Uploading images through WordPress's own
media_handle_upload, with an image-only allowlist. - Encrypting and decrypting values with AES and the vendor's RSA public key.
And then the parts that made it look hostile, all of which are licensing enforcement:
- Integrity checks:
md5_file()on other theme files, writing failures to a theme log. That is the vendor detecting edited (nulled) copies. - A
$_POSTkey: a license key from the activation form, checked against DNS records the vendor controls. delete_option+wp_die403: if the license check fails, the theme wipes its own settings and blocks its settings page.- A footer credit that comes back: a small script re-adds the author link if someone removes it from the page.
- The
evalopcode: used only on constant strings that belong to the anti-debug traps. It never sees request, database or network data.
That is aggressive DRM. It is annoying and it would make a privacy-minded admin unhappy, but it is not a backdoor.
✅ The capability checklist, applied
Running the six backdoor capabilities against the fully disassembled methods:
# Read-only, on the DISASSEMBLY you produced (not the protected file)
grep -nE "eval|assert|create_function|include|require" methods_dis.txt # sinks: what feeds them?
grep -nE "curl_|wp_remote|fsockopen|stream_socket|fopen\(.?http" methods_dis.txt
grep -nE "system|exec|shell_exec|passthru|proc_open|popen" methods_dis.txt
grep -nE "wp_create_user|wp_insert_user|set_role|wp_set_auth_cookie|capabilities" methods_dis.txt
grep -nE "_GET|_REQUEST|_COOKIE|_POST|php://input" methods_dis.txt
grep -nE "file_put_contents|fwrite|unlink|move_uploaded_file" methods_dis.txt
- Run supplied code: no.
evalruns on constants only, and the single include is a WordPress core admin file. - Fetch remote code: no. No HTTP client calls, only a DNS query.
- System commands: no. None at all.
- Grant access: no. No user or role functions.
- Write executables: no. Writes appear only in the protector's shared preamble, and uploads are restricted to images.
- Exfiltrate: no. No outbound data path.
Every row is empty, so the verdict is clean: a legitimate, vendor-protected theme file. It went back to the owner with a short explanation.
⚖️ When the same file would be malware
The same reasoning cuts the other way, which is why "it came with the theme" never ends the analysis. The verdict would flip if any of these turned up:
- A decoded method passes
$_POST,$_COOKIEor a database value into the eval or include opcode. - A license "check" downloads a response and executes it. A remote loader is a backdoor whatever its comments say.
- The theme comes from a "free premium" or nulled download and the licensing code has been stripped or replaced. Nulled software is the delivery method, not a gift.
- The file's hash differs from the copy in the vendor's official package and the extra code holds any of the six capabilities.
🛠️ A practical triage flow for "is this obfuscated file malicious?"
- Get the exact bytes. Work from an untouched copy or the owner's original package, and record the SHA-256 before anything else.
- Never execute it. No
php -r, noincludein a test script, no HTTP request to it on the server. Decode statically. - Compare with upstream when possible. If the vendor's official package has the byte-identical file, you have only established provenance. You still have to judge the code itself.
- Decode until behaviour is visible. Stop when you can read function calls and strings in plain text.
- Run the capability checklist on the decoded code, and trace every sink back to its input.
- Write down the gaps. A method you could not decode is a caveat in the verdict, not something to skip silently.
💡 Takeaways
- Obfuscation is not a verdict in either direction. Vendors and attackers both encrypt code.
- Judge capability: code execution, remote fetch, shell, user creation, executable writes, exfiltration.
- Protected code has to be decoded before you can grep it, because the names you would search for are encrypted.
- Aggressive licensing (integrity checks, settings wipes, forced credits) can look exactly like malware and still be legitimate.
- Nulled copies of the same theme are a real backdoor risk. The legitimate version is only clean because nobody added anything to it.
❓ FAQ
Is an encrypted or obfuscated PHP file in a WordPress theme always malware?
No. Commercial themes and plugins often encrypt licensing code. Attackers obfuscate too, so the file has to be decoded and judged by what it can do: run supplied code, fetch remote code, run commands, create users or write executable files.
Can I just run the file to see what it does?
No. If it is malicious, running it executes the payload, and protected files often include anti-debug traps. Decode it statically on a copy, outside the web server.
The file is identical to the vendor's package. Is that enough?
It proves where the file came from, not that it is safe. It rules out tampering after download, but you still need to know what the vendor's code does.
What should I do with an obfuscated theme file I cannot judge?
Do not delete it or run it blindly. Keep a copy and its SHA-256, get the vendor's original package to compare, and have it decoded statically before deciding. Deleting licensed code can break the theme; keeping a backdoor is worse.