Abrahack's Blog

Gamipress SQLi

Introduction

In this post, I’ll walk through the discovery and analysis of CVE-2024-13496, a high impact vulnerability (CVSS 3.1 Score: 7.5). This vulnerability allows unauthenticated attackers to perform unauthenticated SQLi attacks.

Background

During an offsite security assessment, I noticed the GamiPress plugin installed on a customer’s WordPress instance. As part of my standard methodology, I downloaded version 7.2.1 of the plugin for local analysis, which led to the discovery of an unauthenticated SQLi.

Affected Versions

  • Plugin: GamiPress
  • Version: ≤ 7.3.1

Initial Analysis

My vulnerability discovery process began by identifying potential entry points. The wp_ajax_nopriv_{$action} hook immediately stood out as it allows unauthenticated access.

I focused on the gamipress_get_logs AJAX endpoint, which retrieves logs of user interactions. This function appeared to accept several parameters that might influence database queries:

//gamipress.7.2.1/gamipress/includes/ajax-functions.php
...
function gamipress_ajax_get_logs() {
    // Security check, forces to die if not security passed
    check_ajax_referer( 'gamipress', 'nonce' );

	// Set current page var
    if( isset( $_REQUEST['page'] ) && absint( $_REQUEST['page'] ) > 1 ) {
        set_query_var( 'paged', absint( $_REQUEST['page'] ) );
    }

    $atts = $_REQUEST;

    // Unset non required shortcode atts
    unset( $atts['action'] );
    unset( $atts['page'] );

	// Sanitize
    foreach( $atts as $attr => $value ) {
        $atts[$attr] = sanitize_text_field( $value );
    }

	$atts = shortcode_atts( gamipress_logs_shortcode_defaults(), $atts, 'gamipress_logs' );

	// Send back our successful response
	wp_send_json_success( gamipress_do_shortcode( 'gamipress_logs', $atts ) );

}
add_action( 'wp_ajax_gamipress_get_logs', 'gamipress_ajax_get_logs' );
add_action( 'wp_ajax_nopriv_gamipress_get_logs', 'gamipress_ajax_get_logs' );
...

The $_REQUEST array is passed into the gamipress_logs_shortcode function. The $atts array is passed into the gamipress_logs_shortcode_query function, this function is used to query the database via the CT_Query class.

//gamipress.7.2.1/gamipress/includes/shortcodes/gamipress_logs.php
...
function gamipress_logs_shortcode( $atts = array(), $content = '' ) {
...
    $shortcode = 'gamipress_logs';
...
    // Get the logs query
    $gamipress_template_args['query'] = gamipress_logs_shortcode_query( $atts );
...
}
...
function gamipress_logs_shortcode_query( $args ) {
...
	$query_args = array(
        'type'              => $types,
        'orderby'           => $args['orderby'],
        'order'             => $args['order'],
        'items_per_page'    => $args['limit'],
        'paged'             => max( 1, get_query_var( 'paged' ) ),
    );
...
    return new CT_Query( $query_args );

}

Further inspection of the CT_Query reveal that, the orderby HTTP Request parameter was passed unsafely into an SQL query.

The limitations in place where:

  • white spaces where stripped.
  • no single quotes.
  • no double quotes.
//gamipress.7.2.1/gamipress/libraries/ct/includes/class-ct-query.php
...
    class CT_Query {
        ...
        public function get_results() {
...
            // Order by.
            if ( empty( $q['orderby'] ) ) {
...
            } else {
                $orderby_array = array();
                if ( is_array( $q['orderby'] ) ) {
...
                } else {
                    $q['orderby'] = urldecode( $q['orderby'] );
                    $q['orderby'] = addslashes_gpc( $q['orderby'] );

                    foreach ( explode( ' ', $q['orderby'] ) as $i => $orderby ) {
                        $orderby_array[] = $orderby;
                    }

                    $orderby = implode( ' ' . $q['order'] . ', ', $orderby_array );

                    if ( empty( $orderby ) ) {
                        $orderby = "{$ct_table->db->table_name}.{$ct_table->db->primary_key} " . $q['order'];
                    } elseif ( ! empty( $q['order'] ) ) {
                        $orderby .= " {$q['order']}";
                    }
                }
            }
...
            if ( !empty( $orderby ) )
                $orderby = 'ORDER BY ' . $orderby;

            $found_rows = '';
            if ( !$q['no_found_rows'] && !empty($limits) )
                $found_rows = 'SQL_CALC_FOUND_ROWS';

            $this->request = $old_request = "SELECT $found_rows $distinct $fields FROM {$ct_table->db->table_name} $join WHERE 1=1 $where $groupby $orderby $limits";
...
        }

Proof of Concept

Armed with the knowledge of the limitations in crafting my SQLi payload, I opted for a boolean time based SQLi payload.

CVE-2024-13496 POC

Patch Analysis

After reporting this vulnerability, the vendor addressed it in version 7.3.2 by implementing a whitelist approach. The patch validates the orderby parameter against a predefined list of columns in the gamipress_logs table.

This whitelist approach is a secure solution as it limits the orderby parameter to known-safe values, effectively preventing SQL injection attacks.

Remediation and Disclosure

  • Reported to vendor [31/12/2024]
  • CVE assigned: CVE-2024-13496 [16/01/2025]
  • First fix attempt in version 7.2.2 [21/01/2025]
  • Public Disclosure [21/01/2025]
  • Fixed in version 7.3.2 [18/03/2025]

Conclusion

This was a fun vulnerability to discover!

It reminded me to always check how user input gets into SQL queries, especially in WordPress plugins. Update your GamiPress installation if you haven’t already!


Thanks for Reading! If you found this analysis valuable, consider subscribing to my newsletter for more security insights.