Remote management plug-in is a popular utility tool for WordPress site administrators. It allows users to perform the same operation on multiple sites at the same time, such as updating to the latest release or installing plug-ins. However, in order to implement these operations, the client plug-in needs to grant significant permissions to the remote user. Therefore, it is important to ensure that the communication between the management server and the client plug-in is secure and cannot be forged by an attacker. This article briefly analyzes several available plug-ins. By exploiting their weaknesses, attackers can even completely compromise the site itself running these plug-ins.
ManageWP, InfiniteWP, and CMS Commander
These three services have the same basic client plug-in code (visually it was originally implemented by ManageWp, and then the other two adjusted it), so they all have signature bypass vulnerabilities and can lead to remote code execution.
The management server registers the private key of a client plug-in to calculate the message authentication code of each message, instead of requiring the user to provide administrator credentials (MAC, which we usually see as the MAC address of the hardware. Here is the Message Authentication Code). A message digest is generated when a message passes through a message digest algorithm using a shared secret key. The MAC is then attached to the message and sent out. After receiving it, the receiver uses the shared secret key to calculate the received message, generate MAC2, and then compare it with MAC1. Message digest is used to verify the authenticity and integrity of the message (students who have studied cryptography should know this). This is a good method to ensure communication security. However, there are implementation flaws in the client plug-ins of these three services. This resulted in serious vulnerabilities.
An incoming message authenticated by helper.class.php looks like this:
// $signature is the MAC sent with the message // $data is part of the message if (md5($data . $this->get_random_signature()) == $signature) { // valid message }
Using non-strict equals means that type "spoofing" [type conversion] occurs before comparison. The output of the md5() function is always a string, but if $signature changes to an integer, the type conversion that occurs during comparison can easily forge a matching MAC. For example, if the real MAC starts with "0" or a non-numeric character, then 0 can be matched. If it is "1xxx", then the integer 1 can be matched, and so on. (This is actually a feature of PHP, and of course other languages also have it. When a string and a number are compared for non-strict equality, if the first character is a number, it will be converted into the corresponding integer for comparison. If If it is a character other than 0-9, it will be treated as 0. The official description of php.net: If you compare a number and a string or compare a string involving numerical content, the string will be converted into a numerical value and the comparison will be based on the numerical value. to proceed).
Convert string to numeric value:
When a string is treated as a numerical value, the result and type are as follows:
If the string does not contain '.', 'e' or 'E' and its numeric value is within the range of integers (defined by PHP_INT_MAX), the string will be treated as an integer. In all other cases the value is treated as float.
The beginning of the string determines its value. If the string begins with a legal numeric value, that numeric value is used. Otherwise its value is 0 (zero). Legal values consist of an optional sign, followed by one or more digits (possibly with a decimal point), and then an optional exponent part. The exponent part consists of 'e' or 'E' followed by one or more digits.
<?php var_dump(0 == "a"); // 0 == 0 -> true var_dump("1" == "01"); // 1 == 1 -> true var_dump("10" == "1e1"); // 10 == 10 -> true var_dump(100 == "1e2"); // 100 == 100 -> true var_dump('abcdefg' == 0); // true var_dump('1abcdef' == 1); // true var_dump('2abcdef' == 2); // true ?>
Unfortunately, an attacker can provide an integer as a signature. In init.php, the incoming request will be decoded using base64_decode(), and then the result will be deserialized. The use of Unserialize() means that the type of input data can be controlled. A fake serialization message is as follows:
a:4:{s:9:"signature";i:0;s:2:"id";i:100000;s:6:"action";s:16:"execute_php_code";s: 6:"params";a:2:{s:8:"username";s:5:"admin";s:4:"code";s:25:"exec('touch /tmp/owned') ;";}}
This message uses the integer 0 as a signature, and then uses the execute_php_code provided by the plugin to execute arbitrary PHP code.
$signature = 0; // $data is the action concatenated with the message ID $data = 'execute_php_code' . 100000; if (md5($data . $this->get_random_signature()) == $signature) { // valid message if the output of // md5() doesn't start with a digit }
This forged example may not be used directly. First of all, the key value of id needs to be larger than the value of the previous legitimate message (using the increased message ID is used to prevent replay attacks. Today there are both request forgery and replay attacks. Yes, this reminds me of CSRF, cross-site request forgery, is there a man-in-the-middle attack below?), and secondly, there must be an integer used to match the signature. These two requirements can be broken through brute force cracking.
for i from 100,000 to 100,500: for j from 0 to 9: submit request with id i and signature j
上面的伪代码尝试发送具有很大ID值得虚假消息,并且对每个ID都进行十次单独的数字指纹匹配(前面说到过,对于一个字符串,只要一个数字就可以在比较时进行匹配,这里从0-9是因为每一种情况都能遇到)。
这一缺陷可以通过使用全等运算符[===]和对传入的指纹进行检查来修复。这几个插件服务都通过使用严格的全等运算符进行了修复(php.net的说明:a===b,则a和b值相等,且类型也相等;a==b,在发生类型转换后再判断其值是否相等)。
另外还有一些其他的问题,但是他们还没有采取行动。首先,这一做法是有弱点的(密钥追加到$data,然后进行散列),应该用HMAC(Hash-based Message Authentication Code,以一个密钥和一个消息为输入,生成一个消息摘要作为输出)。其次,仅用于操作的action和消息ID被用于创建签名。这意味着,一个活跃的网络攻击者可以改变消息中的参数而签名依旧是有效的(例如改变execute_php_code消息执行任意代码)。为了进行保护,MAC应该包含整条消息。
(注意,基于MD5的消息摘要是一种后退,可以的话这些插件使用openssl_verify();***2014-04公布出来的Openssl 1.0.f heartbleed漏洞号称世纪级漏洞***)
WORPIT
Worpit是另一个远程管理服务,但它使用从头开始构建的客户端插件,它同样有强制类型转换漏洞,可以让攻击者以管理员权限登陆。
该插件提了远程管理员登陆的方法,使用仅Woprit传递系统可配置的临时的token值。这款插件会检查请求中提供的token值是否和存储在数据库中的值匹配。
if ( $_GET['token'] != $oWpHelper->getTransient( 'worpit_login_token' ) ) { die( 'WorpitError: Invalid token' ); }
令牌是从一次使用的数据库中删除。这意味着大多数的时候都是在数据库中没有令牌。因此,在调用getTransient()方法可能返回false。非严格的比较是,这意味着任何“falsey价值,比如字符串0,将被视为一个有效的令牌。一个例子网址以管理员身份登录:
这个token一经使用就会从数据库中删除,这意味着,大多数时候数据库中是没有token的。因此,对getTransient()方法的调用很可能返回false。非严格的比较也用到了,这意味着任何相当于false的值,例如字符串0会被当做一个有效的token,以管理员身份登陆的例子如:http://victim/?worpit_api=1&m=login&token=0
至此,该站点就为攻击者所控制了,他有权限安装恶意插件或修改已有的插件。
这里的修复方案是使用!==并进行其他检查及从数据库进行检索。
结论:
一定要记住检查用户输入的是预期的类型并在安全性很重要的函数中使用进行严格比较,如检查身份验证令牌。