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.