Abrahack's Blog

Chart Builder LFI

Introduction

In this post, I’ll walk through the discovery and analysis of CVE-2024-10571, a critical vulnerability (CVSS 3.1 Score: 9.8) affecting the Chartify – WordPress Chart Plugin. This vulnerability allows unauthenticated attackers to perform Local File Inclusion (LFI) attacks.

Background

During an offsite security assessment, I noticed the Chartify – WordPress Chart Plugin plugin installed on a customer’s WordPress instance. As part of my standard methodology, I downloaded version 2.9.4 of the plugin for local analysis. This routine audit led to the discovery of a severe security flaw.

So I downloaded a copy of the plugin and decided to perform an audit on a local instance. A few hours later, I discovered an unauthenticated Local File Inclusion (LFI) vulnerability.

Affected Versions

  • Plugin: Chartify – WordPress Chart Plugin
  • Version: ≤ 2.9.4

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.

//chart-builder.2.9.4/includes/class-chart-builder.php
class Chart_Builder {
...
        $this->loader->add_action( 'wp_ajax_ays_chart_admin_ajax', $plugin_admin, 'ays_admin_ajax' );
        $this->loader->add_action( 'wp_ajax_nopriv_ays_chart_admin_ajax', $plugin_admin, 'ays_admin_ajax' );
...

My next step was to find where the ays_admin_ajax function was defined;

//chart-builder.2.9.4/admin/class-chart-builder-admin.php
class Chart_Builder_Admin {
...
    public function display_plugin_charts_page(){
        global $ays_chart_db_actions;

        $action = (isset($_GET['action'])) ? sanitize_text_field( $_GET['action'] ) : '';
        $id = (isset($_GET['id'])) ? absint( esc_attr($_GET['id']) ) : 0;

...
        switch ($action) {
            case 'trash':
...
            case 'add':
                include_once('partials/charts/actions/chart-builder-charts-actions.php');
                break;
...
        }
    }
...
    public function ays_admin_ajax(){
        global $wpdb;

        $response = array(
            "status" => false
        );

        $function = isset($_REQUEST['function']) ? sanitize_text_field( $_REQUEST['function'] ) : null;

        if($function !== null){
            $response = array();
            if( is_callable( array( $this, $function ) ) ){
                $response = $this->$function();

                ob_end_clean();
                $ob_get_clean = ob_get_clean();
                echo json_encode( $response );
                wp_die();
            }

        }

        ob_end_clean();
        $ob_get_clean = ob_get_clean();
        echo json_encode( $response );
        wp_die();
    }
...

From the above snippet, we can see the source code for the ays_admin_ajax function. The function takes an input HTTP parameter function and then checks if the method exists in the Chart_Builder_Admin class, if it exists the method is executed.

At this junction, I started exploring every single method that had no input arguments, the function that caught my attention was the display_plugin_charts_page method.

So let’s take a look at the display_plugin_charts_page method, let’s see what happens where the $action variable is set to add, the file chart-builder-charts-actions.php is included.

Let’s explore the source code of chart-builder-charts-actions.php.

//chart-builder.2.9.4/admin/partials/charts/actions/chart-builder-charts-actions.php
<?php
    require_once( CHART_BUILDER_ADMIN_PATH . "/partials/charts/actions/chart-builder-charts-actions-options.php" );
...
            <?php
                if (!($id === 0 && !isset($_GET['type']) && !isset($_GET['source']))) {
                    require_once( CHART_BUILDER_ADMIN_PATH . "/partials/charts/actions/partials/chart-builder-charts-actions-".stripslashes($chart_source_type).".php" );
                } else {
...

We can observe a potentially dangerous issue in the require_once function: untrusted input is being parsed into the function. So, let’s explore where the $chart_source_type variable is defined. This will take us to a review of the chart-builder-charts-actions.php file.

//chart-builder.2.9.4/admin/partials/charts/actions/chart-builder-charts-actions-options.php
...
            if ($action === "add") {
                // Chart source type
                $chart_source_type = isset($_GET['source']) ? sanitize_text_field($_GET['source']) : 'google-charts';
                // Chart type
                $source_chart_type = isset($_GET['type']) ? sanitize_text_field($_GET['type']) : 'pie_chart';
...

From the above snippet we can see that the $chart_source_type variable is controlled by the source HTTP GET parameter.

Armed with all this information we can craft an adequate proof of concept.

CVE-2024-10571 POC

Patch Analysis

After reporting this vulnerability, the vendor addressed it by implementing sanitization of source HTTP GET parameter:

--- 2.9.4.php   2025-01-09 21:33:06.257850451 +0000
+++ 2.9.5.php   2025-01-09 21:30:00.651972692 +0000
@@ -346,7 +346,9 @@

             if ($action === "add") {
                 // Chart source type
-                $chart_source_type = isset($_GET['source']) ? sanitize_text_field($_GET['source']) : 'google-charts';
+                $allowed_sources = ['google-charts', 'chart-js'];
+                $chart_source_type = isset($_GET['source']) && in_array($chart_source_type, $allowed_sources, true) ? sanitize_text_field($_GET['source']) : 'google-charts';
+
                 // Chart type
                 $source_chart_type = isset($_GET['type']) ? sanitize_text_field($_GET['type']) : 'pie_chart';
             } else {

Remediation and Disclosure

  • Reported to vendor on [30/10/2024]
  • CVE assigned: CVE-2024-10571 [31/10/2024]
  • Fixed in version 2.9.5 [05/11/2024]
  • Public Disclosure [13/11/2024]

Conclusion

This was a fun vulnerability to discover! It made me look twice at error messages and reminded me about the importance of following every lead, no matter how small it seems at first.


Thanks for Reading!