Is This SQL Injection Vulnerability Why a Hacker Would Be Interested in the SendinBlue Subscribe Form And WP SMTP Plugin?
Several days ago we had a request at this website for a file that would be located at /wp-content/plugins/table-maker/readme.txt. Subsequent to that, while reviewing the log files of another website for some work we were doing over at our main business we saw the same file requested. The requested file would be part of the plugin SendinBlue Subscribe Form And WP SMTP. On both websites the IP address also requested a readme.txt for another plugin, which we will be discussing at a later time. Those requests would be seem to be from someone probing for usage of those plugins. A likely reason for that would be a hacker probing for usage of the plugins.
In looking over the plugins we have yet to find some obvious security vulnerability that would be something that would be targeted by a hacker, but we did find that both had poor security leading to security vulnerabilities. Another thing we found with both is that they were using unserialization on data coming from a request from the database. Recently there have been claims that a SQL injection vulnerabilities was somehow being exploited and that lead to PHP object injection when the result from the SQL query susceptible to SQL injection was passed through the unserialize() function. No evidence was presented that was the case though. It is possible that the claim is true. It also possible that there is belief there is an issue that doesn’t really exist (we have seen plenty of instances hackers trying to exploit vulnerabilities that don’t exist and we have security companies failing to understand that). Whatever the case, we did find this plugin has a couple of SQL injection vulnerabilities for which the results is then unserialized.
If someone is aware how the SQL injection issues here could lead to PHP object injection we would love to hear about it, as it would likely help to identify future issues.
Also, if someone sees another issue that might what hackers might be targeting, we would love to hear about that as well.
SQL Injection
The more serious SQL injection issue, since it obvious how it could be exploited, starts with the function sib_parse_request() being set to run during parse_request:
151 | add_action( 'parse_request', array( &$this, 'sib_parse_request' ) ); |
That will cause that function, which is located in the file /sendinblue.php, to run when visiting the frontend or backend of the website.
If the GET input “sib_form” exists that function will cause the file /inc/sib-form-preview.php to be loaded:
962 963 964 | function sib_parse_request(&$wp ) { if ( array_key_exists( 'sib_form', $wp->query_vars ) ) { include 'inc/sib-form-preview.php'; |
That will in turn cause the value of GET input “sib_form” to be passed to the function getForm() if the GET input “action” is not set:
6 7 | $sib_form_id = isset($_GET['sib_form']) ? $_GET['sib_form'] : ''; $sib_preview = isset($_GET['action']) ? $_GET['action'] : ''; |
16 17 | if($sib_preview == '') { $formData = SIB_Forms::getForm($sib_form_id); |
That function, which is located in the file /model/model-forms.php, then uses the passed value in a SQL statement, which permits SQL injection, and the result of that is passed through unserialize():
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | public static function getForm( $frmID = 'new' ) { global $wpdb; if ( 'new' == $frmID ) { // default form. $formData = self::getDefaultForm(); $list = maybe_serialize( array( SIB_API_Manager::get_default_list_id() ) ); $results = array( 'title' => '', 'html' => $formData['html'], 'css' => $formData['css'], 'listID' => $list, 'dependTheme' => '1', 'templateID' => '-1', 'isOpt' => '0', 'isDopt' => '0', 'redirectInEmail' => '', 'redirectInForm' => '', 'date' => date( 'Y-m-d' ), 'successMsg' => $formData['successMsg'], 'errorMsg' => $formData['errorMsg'], 'existMsg' => $formData['existMsg'], 'invalidMsg' => $formData['invalidMsg'], 'attributes' => 'email,NAME', ); } else { $query = 'select * from ' . $wpdb->prefix . self::TABLE_NAME . ' where id=' . $frmID . ';'; $results = $wpdb->get_row( $query, ARRAY_A ); // db call ok; no-cache ok. } if ( is_array( $results ) && count( $results ) > 0 ) { $listIDs = maybe_unserialize( $results['listID'] ); |
The second instance, with possible SQL injection, starts with init() function, which runs during init:
168 | add_action( 'init', array( &$this, 'init' ) ); |
That will cause that function to run when visiting the frontend or backend of the website.
In that function, which is located in the file /sendinblue.php, if the GET input “sib_action” is set to “subscribe” then the subscribe() function will run:
274 275 276 277 278 279 280 281 | function init() { // Sign up process. if ( isset( $_POST['sib_form_action'] ) && ( 'subscribe_form_submit' == $_POST['sib_form_action'] ) ) { $this->signup_process(); } // Subscribe. if ( isset( $_GET['sib_action'] ) && ( 'subscribe' == $_GET['sib_action'] ) ) { SIB_API_Manager::subscribe(); |
The beginning of that function, which is located in the file /inc/sib-api-manager.php, looks as follows:
644 645 646 647 648 649 650 651 652 | public static function subscribe() { $code = isset( $_GET['code'] ) ? esc_attr( sanitize_text_field( $_GET['code'] ) ) : ''; $contact_info = SIB_Model_Users::get_data_by_code( $code ); if ( false != $contact_info ) { $email = $contact_info['email']; $info = maybe_unserialize( $contact_info['info'] ); $list_id = maybe_unserialize( $contact_info['listIDs'] ); |
That code passes the value of the GET input “code” through sanitize_text_field() and esc_attr() functions, which are not designed to protect against SQL injection, and then sets it to the variable $code. It passes that variable to the function get_data_by_code(), then passes the result of that to the variable $contact_info, and finally uses that with the function maybe_serialize().
The function get_data_by_code(), which is located in the file /model/model-users.php, uses the value of the $code variable in SQL statement, which permits SQL injection:
80 81 82 83 84 85 86 87 88 89 90 | public static function get_data_by_code( $code ) { global $wpdb; $query = 'select * from ' . $wpdb->prefix . self::TABLE_NAME . ' where code like "' . $code . '";'; $results = $wpdb->get_row( $query,ARRAY_A ); // db call ok; no-cache ok. if ( is_array( $results ) && count( $results ) > 0 ) { return $results; } else { return false; } } |
The limitation is the use of double quotes around the variable. If you place another double quote in the input being passed there it will be escaped due to WordPress adding magic quotes, so to exploit this you would need to evade that somehow.
There is another get_data_by_code() function in another file that has the same issue, but that function doesn’t look to be used anywhere in the plugin.
We notified the developer of these issues and that it looked like a hacker may be targeting the plugin on Tuesday. We indicated that due to it looking the plugin being targeted by a hacker we would need to disclose things shortly, but we could hold back a bit if we knew that there was a timetable for them being promptly fixed. We got a reply later that day that it was being looked into. Since then we have not heard anything more and the issue has yet to be resolved. By comparison with the other plugin, even though it has nearly two years since it has been previously updated, the developer sent us an updated version to review today (which still needed some improvement).
If you are using our service and would like to continue using the plugin please get in touch with us and we will help apply a temporary fix for this.
Wider Warning
Due to the fact that this issue might be being targeted by hackers we are adding it to the free data that comes with our service’s companion plugin, so that even those not using our service yet can be warned about the issue.
Our plugin is often the only free source of vulnerability information that warns about exploited vulnerabilities, despite it being possible for others to look at the data included in that and the information on our website. One recent example of that involves an arbitrary file upload vulnerability in the most recent version of the plugin PHP Event Calendar, which we disclosed and added to the plugin’s data on November 27. More than two weeks later none of the free sources we are aware, which are the WPScan Vulnerability Database, ThreatPress Vulnerability Database, the plugin CWIS Antivirus Scanner, or the post of the website WPCampus, have included it.
Getting a Fuller Security Review
As the issues shown above the plugin isn’t exactly in great shape when it comes to security and a fuller review would identify more improvements, so if you use the plugin and are concerned with security, it might be a good idea to get a security review done.
From us, you can either order a security review of a plugin or if you use our service you can suggest/vote for plugins to get a review as part of that service.
Proof of Concept
With the following proof of concept it will take varying amounts of time for the page to load depending on how long you specify MySQL sleep function to run.
Make sure to replace “[path to WordPress]” with the location of WordPress, “[form id]” with the ID of one of the plugin’s forms, and “[sleep time]” with how many seconds you want sleep to occur for.
http://[path to WordPress]/?sib_form=[form id] AND SLEEP([sleep time])
Timeline
- December 12, 2017 – Developer notified.
- December 12, 2017 – Developer responds.
I think that the first case could be exploited by passing a value for
sib_form
of the format0 UNION SELECT ... value list with payload ...
. The PHP object injection payload would be passed in the value list as the value of thelistID
column.Or maybe I am missing something?
The problem we have found when we have looked into that approach is getting a valid payload through that. You usually have to deal with quote marks being escaped due to magic quotes, which limits what you can pass through that way. We looked at several methods of trying to deal with that using alternate encoding and they haven’t worked.