WordPress Security Plugin Introduces Security Vulnerability to Websites While Its Protection Against Claimed Threat Is Easily Bypassed
If you were to start looking in to the security of WordPress plugins one thing that might quickly stand out is how often security plugins have security vulnerabilities themselves. At first glance that seems odd, but if you know a little more about those security plugins it starts to make a lot of sense.
Many security plugins are not things that someone that knows much about security would be likely to be developing. For example, despite the claims to the contrary made by security companies, their own data shows that there are not brute force attacks occurring against WordPress admin passwords. So you wouldn’t see someone that knows much about security spending time on that sort of plugin. That makes what we found with the plugin Limit Login Attempts Reloaded not all that surprising.
That plugin is described as:
Limit the number of login attempts that possible both through the normal login as well as using the auth cookies.
WordPress by default allows unlimited login attempts either through the login page or by sending special cookies. This allows passwords (or hashes) to be cracked via brute-force relatively easily.
Limit Login Attempts Reloaded blocks an Internet address from making further attempts after a specified limit on retries has been reached, making a brute-force attack difficult or impossible.
In a previous post we discussed how the plugin Limit Login Attempts, which has 2+ million active installations according to wordpress.org, contains a persistent cross-site scripting (XSS) vulnerability due to its handling of the X-Forwarded-For HTTP header. In looking into that we saw mention of Limit Login Attempts Reloaded and went to check if it was similarly vulnerable. What we found was that it was even more insecure, since unlike Limit Login Attempts, the vulnerability is exploitable without the plugin have to have a setting changed from its default (other than version 2.0.0, which required that setting changed as well). We further found that contrary to its description, it is actually incredibly easy to bypass the limit on logins attempts imposed by the plugin, so the plugin introduces a security vulnerability in exchange for incredibly porous protection against something that really isn’t a threat the average WordPress website should be worried about.
Despite all of that the plugin currently has 70,000+ active installations according to wordpress.org (by comparison the companion plugin for our service, which can help protect websites against a real threat even if not used with our service, is only used on 4,000+ websites).
By default, when there is a failed login attempt it is logged using the function notify_log(), which is located in the file /core/LimitLoginAttempts.php:
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 | public function notify_log( $user_login ) { if ( ! $user_login ) { return; } $log = $option = $this->get_option( 'logged' ); if ( ! is_array( $log ) ) { $log = array(); } $ip = $this->get_address(); /* can be written much simpler, if you do not mind php warnings */ if ( !isset( $log[ $ip ] ) ) $log[ $ip ] = array(); if ( !isset( $log[ $ip ][ $user_login ] ) ) $log[ $ip ][ $user_login ] = array( 'counter' => 0 ); elseif ( !is_array( $log[ $ip ][ $user_login ] ) ) $log[ $ip ][ $user_login ] = array( 'counter' => $log[ $ip ][ $user_login ], ); $log[ $ip ][ $user_login ]['counter']++; $log[ $ip ][ $user_login ]['date'] = time(); if ( isset( $_POST['woocommerce-login-nonce'] ) ) { $gateway = 'WooCommerce'; } elseif ( isset( $GLOBALS['wp_xmlrpc_server'] ) && is_object( $GLOBALS['wp_xmlrpc_server'] ) ) { $gateway = 'XMLRPC'; } else { $gateway = 'WP Login'; } $log[ $ip ][ $user_login ]['gateway'] = $gateway; if ( $option === false ) { $this->add_option( 'logged', $log ); } else { $this->update_option( 'logged', $log ); } } |
The value of the IP address that gets logged comes from the function get_address():
1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 | public function get_address( $type_name = '' ) { if ( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) return $_SERVER['HTTP_X_FORWARDED_FOR']; elseif ( !empty( $_SERVER['HTTP_X_SUCURI_CLIENTIP'] ) ) return $_SERVER['HTTP_X_SUCURI_CLIENTIP']; elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) return $_SERVER['REMOTE_ADDR']; else return ''; } |
If the HTTP header X-Forwarded-For is included with a request that will be returned as the IP address. Since that header comes from the request, it can be set by the requester (unless it is set by a proxy between the requester and the website), so for example, malicious JavaScript code could be used as the value. The value is not sanitized before being stored as a WordPress option.
Once enough attempts have been made to cause a lockout information on that will be shown on the plugin’s admin page (/wp-admin/options-general.php?page=limit-login-attempts). The underlying code that displays does not escape the IP address value, which allows persistent cross-site scripting (XSS) to occur. The data is brought in here:
160 161 | $log = $this->get_option( 'logged' ); $log = LLA_Helpers::sorted_log_by_date( $log ); |
Then the lock outs are split up here:
186 | foreach ( $log as $date => $user_info ): |
And then the IP address is output on this line:
<td class="limit-login-ip"><?php echo $user_info['ip']; ?></td>
The function get_address() is also the key to bypass the limit on login attempts, as by simply providing a different value for X-Forwarded-For with each login attempt the plugin will never see that there have multiple attempts coming from the same IP address since they will see them all as different ones. It should also be possible to lock someone else out if you knew what there IP address is, by simply sending their IP address as the value of X-Forwarded-For for repeated failed requests.
More concerning coming from a security plugin, the developer provides no way of directly contacting them. The only method that it appears you can contact is through the wordpress.org Support Forum, which would lead to moderators complaining, so we are just disclosing this (it be great if WordPress would finally fix the poor moderation of the Support Forum so we would not need to do that and so that we could go back to notifying the Plugin Directory of disclosed vulnerabilities in the current version of WordPress plugins so they don’t remain in directory, as they do now).
Proof of Concept
Attempt to log in to WordPress with the X-Forwarded-For HTTP header set to “<script>alert(document.cookie);</script>” until you are locked out. That header can be set in Chrome using the ModHeader extension.
Afterwards, when visiting the page /wp-admin/options-general.php?page=limit-login-attempts any available cookies will be shown in an alert box.
I absolutely agree that security plugins often do more harm than good – I can’t remember the last time I saw one actually prevent a real problem but I can easily remember many occasions when they have caused problems. This is depressing.
This particular plugin seems to have been fixed, which is great. I doubt that would have happened without you. The reason I installed it though was to reduce server load and it has been successful in that. At one stage I logged attempts from 300,000 different IP addresses in a month so it wasn’t trivial – more a DDoS than a brute force attack. So I’m leaving it enabled until I find an alternative. Just sayin’.
Keep up the good work and thank you.