Headline
CVE-2023-5843: dfads.class.php in ads-by-datafeedrcom/tags/1.1.3/inc – WordPress Plugin Repository
The Ads by datafeedr.com plugin for WordPress is vulnerable to Remote Code Execution in versions up to, and including, 1.1.3 via the ‘dfads_ajax_load_ads’ function. This allows unauthenticated attackers to execute code on the server. The parameters of the callable function are limited, they cannot be specified arbitrarily.
1<?php23/**4 * This class is responsible for querying the ads and5 * formatting their output.6 */7class DFADS {89 private $args;1011 // This is called via the dfads() function. It puts everything in motion.12 function get_ads( $args ) {13 14 $this->set_args( $args );15 16 // Return JS output for avoiding caching issues.17 // Return this before running query() to avoid:18 // - The query being run twice.19 // - Impressions being counted twice.20 if ( $this->args[‘return_javascript’] == ‘1’ ) {21 return $this->get_javascript();22 }23 24 // Get ads.25 $ads = $this->query();26 27 if ( empty($ads) ) { return false; }28 29 // Count impressions.30 $this->update_impression_count( $ads );31 32 // Return user’s own callback function.33 if ( function_exists( $this->args[‘callback_function’] ) ) {34 return call_user_func_array($this->args[‘callback_function’], array( $ads, $this->args ));35 }36 37 // Return default output function.38 return $this->output( $ads, $this->args );39 }40 41 // Set up default arguements that can be modified in dfads().42 function set_args( $args ) {43 44 // Undo anything that the text editor may have added.45 $args = str_replace ( 46 array( "&", "<", ">", ""e;", “%2C” ), 47 array( "&", "", "", "\"", “,” ), 48 $args 49 );50 51 // Now reformat52 $args = htmlentities( $args );53 54 // Create array of values.55 $args = explode("&", $args);5657 $new_args = array();58 foreach ($args as $arg) {59 $arr = explode( "=", $arg, 2 );60 $k = $arr[0];61 $v = $arr[1];62 // This section gets rid of the pesky “#038;” charcters WP changes “&” to.63 $k = str_replace( array( “#038;” ), array( “” ), $k );64 $new_args[$k] = $v;65 }6667 $defaults = array (68 ‘groups’ => '-1’,69 ‘limit’ => '-1’,70 ‘orderby’ => 'random’,71 ‘order’ => 'ASC’,72 ‘container_id’ => '’,73 ‘container_html’ => 'div’,74 ‘container_class’ => '’,75 ‘ad_html’ => 'div’,76 ‘ad_class’ => '’,77 ‘callback_function’ => '’,78 ‘return_javascript’ => ‘’,79 );80 81 $this->args = wp_parse_args( $new_args, $defaults );82 }83 84 // Build the SQL query to get the ads.85 function query() {86 // http://codex.wordpress.org/Displaying_Posts_Using_a_Custom_Select_Query87 global $wpdb;88 $tax_sql = $this->sql_get_taxonomy();89 $tax_join = $tax_sql[‘JOIN’];90 $tax_and = $tax_sql[‘AND’];91 $limit = $this->sql_get_limit();92 $orderby = $this->sql_get_orderby();93 $order = $this->sql_get_order();94 $sql = “95 SELECT96 p.*, 97 imp_count.meta_value AS ad_imp_count,98 imp_limit.meta_value AS ad_imp_limit,99 start_date.meta_value AS ad_start_date,100 end_date.meta_value AS ad_end_date101 FROM $wpdb->posts p102 LEFT JOIN $wpdb->postmeta AS imp_limit 103 ON p.ID = imp_limit.post_id 104 AND imp_limit.meta_key = '_dfads_impression_limit’105 LEFT JOIN $wpdb->postmeta AS imp_count 106 ON p.ID = imp_count.post_id107 AND imp_count.meta_key = '".DFADS_METABOX_PREFIX."impression_count’108 LEFT JOIN $wpdb->postmeta AS start_date 109 ON p.ID = start_date.post_id 110 AND start_date.meta_key = '_dfads_start_date’111 LEFT JOIN $wpdb->postmeta AS end_date 112 ON p.ID = end_date.post_id113 AND end_date.meta_key = '_dfads_end_date’114 $tax_join115 WHERE p.post_status = 'publish’116 AND p.post_type = 'dfads’117 AND ( 118 CAST(imp_limit.meta_value AS UNSIGNED) = 0 119 OR CAST(imp_count.meta_value AS UNSIGNED) < CAST(imp_limit.meta_value AS UNSIGNED) 120 OR CAST(imp_count.meta_value AS UNSIGNED) IS NULL121 )122 AND (123 CAST(start_date.meta_value AS UNSIGNED) IS NULL124 OR “.time().” >= CAST(start_date.meta_value AS UNSIGNED)125 )126 AND (127 CAST(end_date.meta_value AS UNSIGNED) IS NULL128 OR “.time().” <= CAST(end_date.meta_value AS UNSIGNED)129 )130 $tax_and131 GROUP BY p.ID132 ORDER BY $orderby $order 133 LIMIT $limit134 “;135136 return $wpdb->get_results( $sql, OBJECT ); 137 }138 139 // Build the taxonomy portion of the SQL statement.140 function sql_get_taxonomy() {141 global $wpdb;142 $sql = array();143 $sql[‘JOIN’] = '’;144 $sql[‘AND’] = ‘’;145 146 if ( !$group_ids = $this->get_group_term_ids( $this->args[‘groups’] ) ) {147 return $sql;148 }149 150 $ids = implode( ",", $group_ids );151 $sql[‘JOIN’] = " LEFT JOIN $wpdb->term_relationships AS tr ON (p.ID = tr.object_id) LEFT JOIN $wpdb->term_taxonomy AS tax ON (tr.term_taxonomy_id = tax.term_taxonomy_id) ";152 $sql[‘AND’] = " AND tax.taxonomy = ‘dfads_group’ AND tax.term_id IN($ids) ";153 return $sql;154 }155156 // Build the LIMIT portion of the SQL statement.157 function sql_get_limit() {158 return ($this->args[‘limit’] == '-1’) ? 999 : intval($this->args[‘limit’]);159 }160 161 // Build the ORDER BY portion of the SQL statement.162 function sql_get_orderby() {163 $orderby_defaults = self::orderby_array();164 return $orderby_defaults[$this->args[‘orderby’]][‘sql’];165 }166167 // Build the ORDER portion of the SQL statement. 168 function sql_get_order() {169 return ($this->args[‘order’] == ‘ASC’) ? ‘ASC’ : 'DESC’;170 }171 172 // A set of possibly values for the ORDER BY part of the SQL query.173 public static function orderby_array() {174 return array(175 ‘ID’ => array( 'name’=>’ID’, ‘sql’=>’p.ID’ ),176 ‘post_title’ => array( 'name’=>’Ad Title’, ‘sql’=>’p.post_title’ ),177 ‘post_date’ => array( 'name’=>’Date Created’, ‘sql’=>’p.post_date’ ),178 ‘post_modified’ => array( 'name’=>’Date Modified’, ‘sql’=>’p.post_modified’ ),179 ‘menu_order’ => array( 'name’=>’Menu Order’, ‘sql’=>’p.menu_order’ ),180 ‘impression_count’ => array( 'name’=>’Impression Count’, 'sql’=>’CAST(imp_count.meta_value AS UNSIGNED)' ),181 ‘impression_limit’ => array( 'name’=>’Impression Limit’, 'sql’=>’CAST(imp_limit.meta_value AS UNSIGNED)' ),182 ‘start_date’ => array( 'name’=>’Start Date’, 'sql’=>’CAST(start_date.meta_value AS UNSIGNED)' ),183 ‘end_date’ => array( 'name’=>’End Date’, 'sql’=>’CAST(end_date.meta_value AS UNSIGNED)' ),184 ‘random’ => array( 'name’=>’Random’, 'sql’=>’RAND()' ),185 );186 }187 188 // This loops through all ads returned and updates their impression count (default: if user != admin).189 function update_impression_count( $ads ) {190 191 // Don’t count if admin AND admin impressions don’t count.192 if ( current_user_can(‘level_10’) ) {193 $output = get_option( ‘dfads-settings’ );194 if ( !isset( $output[‘dfads_enable_count_for_admin’] ) || $output[‘dfads_enable_count_for_admin’] != ‘1’ ) {195 return;196 }197 }198 199 // Don’t count if we’ve already set a block_id.200 // This is to handle impresion counts for when ‘return_javascript’ equals 1201 // because ‘return_javascript’ returns a <script> and <noscript> tag and202 // we have to avoid the impression being counted twice. So we store this203 // ad groups unique “block_id” as a transient value to check subsequent calls204 // for the same ad block. If it’s already set, we don’t count again.205 if ( isset( $this->args[‘_block_id’] ) ) {206 if ( get_transient( ‘dfad_’ . $this->args[‘_block_id’] ) ) {207 return;208 } else {209 set_transient( ‘dfad_’ . $this->args[‘_block_id’], true, 5 );210 }211 }212 213 foreach ($ads as $ad) {214 update_post_meta($ad->ID, DFADS_METABOX_PREFIX.’impression_count’, (intval($ad->ad_imp_count)+1));215 }216 217 }218 219 // This formats and outputs the ads. This is overridable if the user has defined $this->args[‘callback_function’]220 function output( $ads, $args ) {221 222 $ad_count = count( $ads );223 $i = 1;224 $html = '’;225 226 // Determine if we should include tags for containers and ad wrappers.227 // If 'none’, then we remove the tag from output.228 $container_html = ( $args[‘container_html’] == ‘none’ ) ? ‘’ : $args[‘container_html’];229 $ad_html = ( $args[‘ad_html’] == ‘none’ ) ? ‘’ : $args[‘ad_html’];230 231 // If contain_html is not empty, get container’s opening tag.232 if ( $container_html != ‘’) {233 $html .= $this->open_tag( $container_html, $args[‘container_class’], $args[‘container_id’] );234 }235 236 // Loop through ads.237 foreach ($ads as $ad) {238 239 $first_last = ' ';240 if ( $i == 1 ) {241 $first_last = ' dfad_first ';242 } elseif ( $i == $ad_count ) {243 $first_last = ' dfad_last ';244 }245 246 // If ad_html is not empty, get the ads’s opening tag.247 if ( $ad_html != ‘’) {248 $html .= $this->open_tag( $ad_html, 'dfad dfad_pos_’.$i.$first_last.$args[‘ad_class’], $args[‘container_id’].’_ad_’.$ad->ID );249 }250 251 // Get ad content and append ad content to html block252 $html .= apply_filters( 'dfads_ad_post_content’, $ad->post_content, $ad, $args );253 254 // If ad_html is not empty, get the ads’s closing tag.255 if ( $ad_html != ‘’) {256 $html .= $this->close_tag( $ad_html );257 }258 259 $i++;260 }261 262 // If contain_html is not empty, get container’s closing tag.263 if ( $container_html != ‘’) {264 $html .= $this->close_tag( $container_html );265 }266267 return apply_filters( 'dfads_ads_html_block’, $html, $ads, $args );268 }269 270 function get_javascript() {271 272 $id = ( $this->args[‘container_id’] != ‘’ ) ? $this->args[‘container_id’] : 'df’.$this->generate_random_string( 5 );273 274 // Set ‘return_javascript’ to ‘0’ or else we end up with an infinite loop.275 $args = $this->args;276 $args[‘return_javascript’] = '0’;277 $args[‘_block_id’] = $id;278 $args[‘container_html’] = 'none’; // Set to ‘none’ so we don’t display the container HTML twice.279 280 return ‘281 <’.$this->args[‘container_html’].’ id="’.$id.’” class="dfads-javascript-load"></’.$this->args[‘container_html’].’>282 <script>283 (function($) { 284 $(“#’.$id .’”).load(“’.admin_url( 'admin-ajax.php?action=dfads_ajax_load_ads&’.http_build_query( $args ) ).’” ); 285 })( jQuery );286 </script>287 <noscript>’.dfads( http_build_query( $args ) ).’</noscript>288 ';289 290 }291 292 /**293 * This gets the group IDs.294 * 295 * $groups could be any of the following:296 * - '’297 * - 'groups=’298 * - 'groups=1’299 * - 'groups=sidebar’300 * - 'groups=1,2’301 * - 'groups=sidebar,header’302 * 303 * "-1” is equivalent to "All".304 */305 function get_group_term_ids( $groups=false ) {306 307 if (!$groups || $groups == '-1’ || $groups == ‘’) { return false; }308 309 $groups = explode(",", $groups);310 $group_ids = array();311 312 foreach( $groups as $group ) {313 314 // Try to get term ID from id.315 if ($group_obj = get_term_by( 'id’, $group, ‘dfads_group’ )) {316 $group_ids[] = intval($group_obj->term_id);317 continue;318 }319 320 // Try to get term ID from slug.321 if ($group_obj = get_term_by( ‘slug’, $group, ‘dfads_group’ )) {322 $group_ids[] = intval($group_obj->term_id);323 continue;324 }325 326 // Try to get term ID from name.327 if ($group_obj = get_term_by( ‘name’, $group, ‘dfads_group’ )) {328 $group_ids[] = intval($group_obj->term_id);329 continue;330 }331 }332 333 if (!empty($group_ids)) {334 return $group_ids;335 }336 337 return false;338 }339 340 // Formats an opening HTML tag with CSS classes and IDs.341 function open_tag( $tag=’div’, $class=’’, $id=’’ ) {342 $tag = ($tag == ‘’) ? ‘div’ : trim($tag);343 $class = ($class != ‘’) ? ' class="’.trim(esc_attr($class)).’"’ : ‘’;344 $id = ($id != ‘’) ? ' id="’.trim(esc_attr($id)).’"’ : '’;345 return ‘<’.$tag.$class.$id.’>’;346 }347348 // Formats a closing HTML tag.349 function close_tag( $tag=’div’ ) {350 $tag = ($tag == ‘’) ? ‘div’ : trim($tag);351 return '</’.$tag.’>’;352 }353 354 // Helper function (http://stackoverflow.com/a/4356295)355 function generate_random_string( $length=10 ) {356 $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ’;357 $randomString = '’;358 for ( $i = 0; $i < $length; $i++ ) {359 $randomString .= $characters[rand(0, strlen($characters) - 1)];360 }361 return $randomString;362 }363}