7 Ağustos 2013 Çarşamba

prepare methodunu kullanarak sql injection




Kullanılan Araçlar :

PHP 5.4.6
Linux Mint
Sublime Text
Zap proxy

PHP uygulamalarının büyük bir bölümü veritabanı işlemlerini mysql_* fonksiyonları üzerinden gerçekleştirmektedir. Bununla ilgili Github içinde ufak bir arama gerçekleştirdik.

Github dork : "extension:php mysql_query"


 Yaklaşık 316 bin kod parçasında mysql_query() fonksiyonu kullanılıyor.

Bu hatırı sayılır bir rakam olmasına rağmen php geliştiricileri farklı düşünüyor:

This extension is deprecated as of PHP 5.5.0, and will be removed in the future. Instead, the MySQLi or PDO_MySQLextension should be used. See also MySQL: choosing an API guide and related FAQ for more information. Alternatives to this function include:

Kaynak : http://tr1.php.net/mysql_query

Gördüğümüz kadarıyla PHP 5.5.0 ile birlikte mysql_query fonksiyonu  tedavülden kalkacak ve  PDO yapısına geçmek zorunda kalacağız. mysql_query kullanan geliştiriciler için kodlarını yenilemek meşakkatli olacak gibi. Bardağın dolu tarafından bakalım. Kısa bir zaman önce öğrenmeye başladığım Laravel4 dökümantasyonunda şöyle bir ifade geçiyor:

Not: Laravel sorgu oluşturucusu, uygulamanızı SQL enjeksiyon saldırılarına karşı korumak için PDO parametre bağlayıcı kullanmaktadır. Bağlayıcı olarak geçirilen yazıların temizlenmesine gerek yoktur.

Bir Çatı(Framework) kullanmak, SQL enjeksiyon gibi bir zaafiyete karşı kesin çözüm gibi duruyor. Çatı(Framework) kullan ve güvende ol! Bu yaklaşımın büyük ölçüde doğru olduğunu düşünüyorum. Ama PDO extension'ı kendi sınıf yapısıyla deneyimlemek isteyen bir geliştirici için ufak bir mantık hatası, büyük sıkıntıları da beraberinde getirebilir. Aşağıdaki örnekte ufak bir PDO bağlantı sınıfı ve SQL enjeksiyon zafiyeti içeren bir kod parçası yazdım. Satır satır inceleyelim:

PDO bağlantısını sağlayan statik sınıf:

class DB {
        
        protected static $instance;

        protected function __construct() {}
        
        public static function getInstance() {
        
                if(empty(self::$instance)) {
                        
                        $db_info = array(
                                "db_host" => "localhost",
                                "db_port" => "3306",
                                "db_user" => "root",
                                "db_pass" => "",
                                "db_name" => "",
                                "db_charset" => "UTF-8"
                        );
                        
                        
                        try {                          
                               self::$instance = new PDO("mysql:host=".$db_info['db_host'].';port='.$db_info['db_port'].';dbname='.$db_info['db_name'], $db_info['db_user'], $db_info['db_pass']);
  //self::$instance = new PDO("mysql:host=localhost;, $db_info['db_user'], $db_info['db_pass']);
                               #self::$instance->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );  
                               self::$instance->setAttribute( PDO::MYSQL_ATTR_USE_BUFFERED_QUERY,true);  
                                self::$instance->query('SET NAMES utf8');
                                self::$instance->query('SET CHARACTER SET utf8');

                        }  
                        catch(PDOException $e) {  
                                echo $e->getMessage();  
                        }                          
                        
                }
                
                return self::$instance;
        
        }
   

} 

Zaafiyete neden olan kod parçası :

//user-agent bilgisi kullanıcıdan alınıyor.
$var = $_SERVER['HTTP_USER_AGENT'];
// DB sınıfı çağrılıyor.
$connection = DB::getInstance();
//SQL sorgusu yazılıyor.
$query = "INSERT INTO `authors`(`name`, `bio`)  VALUES ( 'one', $var )";
//SQL sorgusu işletiliyor.
$result = $connection->prepare($query);
$result->execute();
echo "Kayıt girildi...!\n";  

Kodu sömürmek için saldırı vektörümüz şu şekilde olmalıdır:

(SELECT 'TEST' REGEXP (SELECT CASE WHEN (substr(version(),1,1)=5) THEN '.*' ELSE '*' END))

SQL sorgusunda; subquery, regex ve boolean condition 'ı birlikte kullanıyoruz. Eğer MySQL sürümü 5.x ise dönen değer "1" olacaktır aksi takdirde #1139 - Got error 'repetition-operator operand invalid' from regexp  gibi bir hata alacaksınız.

Zap proxy ile "user-agent" bilgisini şu şekilde değiştirelim :



Veriyi sunucuya ilettikten sonra phpmyadmin ile sonuca bakıyoruz.


"bio" kolonunda 1 rakamını görüyoruz. Demekki veritabanı sunucusu 5.x sürümünü kullanıyor.

Durum vahim, mevcut yazılımda bir enjeksiyon noktası tespit etmiş olduk.

Peki nasıl korunabiliriz?
  • Güvenli bir sorgu oluşturmak istiyorsak PDO'i Prepared Statements (Hazır Deyimler) ile birlikte kullanmamız gerekmektedir. Ufak bir örnek yapalım:

$q = $db->prepare('INSERT INTO kullanici(ad, soyad, yas) VALUES (:adi, :soyadi, :yasi)');

$q->bindParam(':adi',$ad);

$q->bindParam(':soyadi',$soyad);

$q->bindParam(':yasi',$yas);


Değiştirgeleri veya soru imlerini PHP değişkenleri ile ilişkilendirmek için PDOStatement::bindParam() yöntemini kullanın; değişkenleri, değerleriyle girdi olarak ve çıktı değerini almak için kullanabilirsiniz.

Kaynak : http://www.php.net/manual/tr/pdostatement.execute.php

EOF

4 yorum:

  1. Tırnak arasına koysak (ayrıca özel karakterler escapleniyor mu burada mysql_real_escape fonksiyonundaki gibi ?):

    $query = "INSERT INTO `authors`(`name`, `bio`) VALUES ( 'one', '$var' )";

    YanıtlaSil
    Yanıtlar
    1. Prepared statementlar tek çift tırnakları sorgusuna almaz ve escape işlemini otomatik olarak yapar.

      Sil
  2. burada bindParam fonksiyonu yerine array kullanarak sorgu içerisine parametreleri gömmek arasında bir fark var mı? bahsettiğim kullanım şu şekilde:

    $db->prepare("select * from tablo where alan = :alan_degeri");
    $db->execute(array(":alan_degeri" => "istenilen değer"));

    YanıtlaSil
    Yanıtlar
    1. prepared statement yapmak için tek bir yöntem yok. http://php.net/manual/en/pdostatement.execute.php adresindeki örnekleri inceleyebilirsiniz.

      Sil