Technical write-up on CVE-2021-24299

I was scrolling through the extensive list of WordPress plugins while being hungry, and there it was. My next snack would be the ReDi Restaurant Reservation plugin, which would hopefully still my security research hunger. In the following write-up, I’ll explain a vulnerability that I found in the ReDi Restaurant Reservation WordPress plugin CVE-2021-24299. This vulnerability allows an unauthenticated attacker to store malicious JavaScript code into the external ‘upcoming reservations’ webpage where restaurant reservations of the plugin are listed.

ReDi Restaurant Reservation is a WordPress plugin that provides a restaurant reservation system, where users can make and edit reservations. The admin of the website where the plugin is installed can look at overviews of reservations made. The plugin also provides an external website that the website owner can visit, from a tablet, to view and edit all the reservations for that day. ReDi Restaurant Reservation version 21.0307 and earlier contains an Unauthenticated Stored Cross-Site Scripting (XSS) vulnerability.

How the vulnerability works

Let’s have a bite of this snack! How does this vulnerability work? The plugin provides users the functionality to book a reservation for the restaurant. A user just has to visit the reservation page; it looks like this.

When the reservation form is submitted, a POST request is received by the website. The PHP code below shows how the data in the POST request is processed and saved to local variables. The code shows data is not sanitized or validated before being saved to the variables. However, the UserComments string length is capped at 250 characters.

$comment .= mb_substr(self::GetPost('UserComments', ''), 0, 250);

$params = array(
    'reservation' => array(
        'StartTime' => $startTimeISO,
        'EndTime' => $endTimeISO,
        'Quantity' => $persons,
        'UserName' => self::GetPost('UserName'),
        'UserEmail' => self::GetPost('UserEmail'),
        'UserComments' => $comment,
        'UserPhone' => self::GetPost('UserPhone'),
        'UserProfileUrl' => $user_profile_image,
        'Name' => 'Person',
        'Lang' => str_replace('_', '-', self::GetPost('lang')),
        'CurrentTime' => $currentTimeISO,
        'Version' => $this->version,
        'PrePayment' => 'false',
        'Source' => 'HOMEPAGE'
    )
);

Next, the saved variables are pushed to the database. Also note here, that variables are not sanitized or validated before being pushed to the database. This means the strings we submit through the form for the variables UserName, UserPhone, UserEmail and UserComments will be saved to the database without changes.

function save_reservation($params, $reservation)
{
    if (isset($reservation['Error']))
    {
        return;
    }

    global $wpdb;
    
    $reservParams = $params['reservation'];

    $wpdb->insert( $this->table_name, [ 
        'reservation_number' => $reservation['ID'],
        'name'               => $reservParams['UserName'],
        'phone'              => $reservParams['UserPhone'],
        'email'              => $reservParams['UserEmail'],
        'date_from'          => $reservParams['StartTime'],
        'date_to'            => $reservParams['EndTime'],
        'guests'             => $reservParams['Quantity'],
        'comments'           => $reservParams['UserComments'],                 
        'prepayment'         => $reservParams['PrePayment'],                   
        'currenttime'        => $reservParams['CurrentTime'],                   
        'language'           => $reservParams['Lang']   
    ] );

Let’s see how we can exploit this vulnerability. We submit a reservation including the following JavaScript payload.

<script>alert("XSS")</script>

This payload, when executed, will create an alert in the browser containing the text ‘XSS’. However, between the script tags, you can put any JavaScript code. We fill in the form, and in the ‘Comment’ field we type our JavaScript payload.

We see the following success alert:

So we submitted the reservation form to the website. How are the submitted variables passing through the code?

When we submitted the form, as shown above, a POST request is sent to the server. The PHP code gets the data from the POST request by using GetPost(‘variable’). Below you can see how the variables are set with the data we submitted.

$comment .= '<script>alert("XSS")</script>'

$params = array(
    'reservation' => array(
        'StartTime' => $startTimeISO,
        'EndTime' => $endTimeISO,
        'Quantity' => $persons,
        'UserName' => 'Test',
        'UserEmail' => 'test@test.com',
        'UserComments' => '<script>alert("XSS")</script>',
        'UserPhone' => '06123456789',
        'UserProfileUrl' => $user_profile_image,
        'Name' => 'Person',
        'Lang' => str_replace('_', '-', 'en',
        'CurrentTime' => $currentTimeISO,
        'Version' => $this->version,
        'PrePayment' => 'false',
        'Source' => 'HOMEPAGE'
    )
);

Next, the $params array variable gets pushed to the database. Below you can see how the variables are pushed to the database.

function save_reservation($params, $reservation)
{
    if (isset($reservation['Error']))
    {
        return;
    }

    global $wpdb;
    
    $reservParams = $params['reservation'];

    $wpdb->insert( $this->table_name, [ 
        'reservation_number' => '001',
        'name'               => 'Test',
        'phone'              => '06123456789',
        'email'              => 'test@test.com',
        'date_from'          => '14-04-2021',
        'date_to'            => '14-04-2021',
        'guests'             => '2',
        'comments'           => '<script>alert("XSS")</script>',                 
        'prepayment'         => 'true',                   
        'currenttime'        => '20:20',                   
        'language'           => 'en'   
    ] );

So, our JavaScript payload is saved to the database. The next question is, how is the payload loaded into the webpage?

The ReDi Restaurant Reservation plugin’s admin menu has a submenu called ‘Upcoming (Tablet PC)’. This is a webpage where you can view the reservations made for a specific period. This page isn’t a WordPress webpage, but an external page that is loaded within an iframe, as can be seen in the PHP code.

function redi_restaurant_admin_upcoming()
     {
         $iframe_url = '//upcoming.reservationdiary.eu/Entry/' . $this->ApiKey;
         require_once(REDI_RESTAURANT_TEMPLATE . 'iframe.php');
     }

The URL that is loaded within the iframe takes the URL https://upcoming.reservationdiary.eu/Entry/ and appends it with the API key that is registered in your ReDi Restaurant Reservation plugin. When visiting this URL, it shows all the made reservations for a specific period.

The code of the website loaded within an iframe upcoming.reservationdiary.eu isn’t part of the source code of the plugin. Therefore, I can’t show you in detail how the loading of the reservation works on the webpage. However, when the https://upcoming.reservationdiary.eu/Entry/[API-key]/ is loaded, and you click on ‘View upcoming reservations’ the JavaScript payload is executed twice.

Impact

The unauthenticated stored XSS vulnerability in ReDi Restaurant Reservation version 20.0307 poses a risk for website owners who use this plugin. A malicious attacker can make a reservation that includes malicious JavaScript code in the comment field. When the website owner visits the ‘Upcoming (Tablet PC)’ submenu or https://upcoming.reservationdiary.eu/Entry/[API-key]/ and clicks on the ‘View upcoming reservations’ button, the JavaScript payload executes twice.

This makes it possible for malicious attackers to for example steal the plugin API key and potentially steal information about customers that made reservations, steal cookies, or other sensitive data.

Proof of Concept

Conclusion

ReDi Restaurant Reservations 20.0307 and earlier versions contain an unauthenticated stored XSS vulnerability. This makes it possible to store malicious JavaScript code on the external ‘upcoming reservations’ webpage where reservations are listed.

The vulnerability exists because the ‘Comment’ message field of the reservation form does not sanitize and validate user input, therefore code can be saved to the database. In addition, the ‘Comment’ field string that is saved in the database is loaded into the webpage without special characters (such as <, >, &, “, and ‘) being converted to HTML entities, which leads to the code being loaded into the webpage. Therefore, the code will be loaded and executed into the webpage where made reservations are listed.

Timeline

15 April, 2021: WPScan – Vendor contacted
21 April, 2021: WPScan – Escalated to WordPress
25 April, 2021: Vulnerability fixed by vendor
06 May, 2021: WPScan notified me that the disclosure is delayed due to the severity of the vulnerability
10 May, 2021: Disclosure on WPScan and CVE-2021-24299 assigned
23 May, 2021: Proof of Concept disclosure

References

https://wpscan.com/vulnerability/fd6ce00b-8c5f-4180-b648-f47b37303670
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-24299
https://wordpress.org/plugins/redi-restaurant-reservation/
https://www.exploit-db.com/exploits/49903