Showing posts with label ruby. Show all posts
Showing posts with label ruby. Show all posts

Saturday, October 6, 2012

Verifying NTP Reserved Mode Denial of Service Vulnerability

I recently needed to check a NTP Reserved Mode Denial of Service vulnerability CVE-2009-3563, but without causing the DoS condition on the production server.  Using Metasploit’s auxillary module auxiliary/dos/ntp/ntpd_reserved_dos was not an option so I wrote my own Ruby script to assess the remote server. This script verifies the returned UDP packet content to determine presence of vulnerability and is shared below.


#Author: Gursev Singh Kalra  
require 'socket'
TIMEOUT = 5 

if(ARGV.count != 1)
 puts "[-] Target host not provided. Usage: ntp.rb <target_server>"
 exit
end

target_server = ARGV[0]
target_port = 123

socket = nil
response = nil

begin
 test_string = "\x97\x00\x00\x00\xAA\x00\x00\x00"
 socket = UDPSocket.open
 socket.send(test_string, 0, target_server, target_port)
 if select([socket], nil, nil, TIMEOUT)
  response = socket.recvfrom(10)
 end
rescue (IOError ex)
 puts ex.to_s
ensure
 socket.close if(socket)
end

if(response && response[0].index("\x97\x00\x00\x00"))
 puts "[+] Vulnerable to NTP Mode 7 Request Denial Of Service"
else
 puts "[-] Not vulnerable to NTP Mode 7 Request Denial Of Service "
end

Figure 1: Image shows request capture in wireshark

Figure 2: Image shows response capture in wireshark

Figure 3: Image shows script in action

Saturday, February 25, 2012

Sqlitespy for Sqlite Database analysis

Sqlite is the ubiquitous database for mobile applications on iPad, iPhone and Android. It is also used by certain internet browsers, web application frameworks and software products for their local storage needs. While doing penetration tests, we often see sensitive information like usernames, passwords, account numbers, SSN etc… insecurely stored in these databases. Thus, every penetration test requires comprehensive analysis of the local databases being used.


While analyzing databases, a penetration tester repeatedly does the following:
  1. Opens the database in sqlite reader (sqlite3 or other readers)
  2. Views various tables and columns to understand database layout and schema.
  3. Analyzes the storage for sensitive information.
As the number and size of database increases, the analysis time increases exponentially. To escape the recurring pain, I wrote a ruby script to automate this process. The script achieves the following:
  1. Analyzes multiple databases in a single run.
  2. Queries and displays database schema.
  3. Provides an option to run search on Table and Column Names for quick analysis.
  4. Performs case-insensitive regular expression search (default). This can be controlled with command line options to one’s requirements.
  5. Displays Database, Tables and Row Number reference for every successful match.
  6. Dumps database rows on a successful match.
  7. Looks for search strings in the following:
    • Table Name
    • Column Names
    • Actual Data
Sqlitespy dependencies are listed below:
  1. Ruby
  2. Sequel Gem
  3. Sqlite3



Figure 1: Image shows sqlitespy help


Figure 2: Image shows sqlite sample run with multiple search strings and row information dump for a successful match


Figure 3: Image shows sqlitespy sample run with minimal information


Figure 4: Image shows sqlite database schema dump

Sqlitespy Code Follows:

 #Author: Gursev Singh Kalra  
 require 'rubygems'  
 require 'optparse'  
 require 'ostruct'  
 require 'sequel'  
   
   
 class CmdLineOptions  
   
  def self.parse(args)  
   options = OpenStruct.new  
   options.dbs = []  
   options.sstrings = []  
   options.show_schema = false  
   options.case_sensitive = false  
   options.exact = false  
   options.verbose = false  
   options.rowdump = false  
   options.metadata = false  
   
   opts = OptionParser.new do |opts|  
   opts.banner = "Usage: sqlitespy.rb [options]\n\nSpecific Options:"  
   
   opts.on("-d", "--database DATABASE_PATH",  
    "Sqlite database to analyze.") do |db|  
    options.dbs << db  
   end  
   
   opts.on("-s", "--show-schema", "Show database schema") do |show|  
    options.show_schema = show;  
   end  
   
   opts.on("--find x,y,z", Array, "Strings to search") do |list|  
    options.sstrings = list  
   end  
   
   opts.on("-c", "--case-sensitive", "Perform case sensitive search. Default is case insensitive.") do |case_sensitive|  
    options.case_sensitive = case_sensitive;  
   end  
   
   opts.on("-e", "--exact--match", "Perform exact match for the search strings") do |v|  
    options.exact = v;  
   end  
   
   opts.on("-r", "--row-dump", "Dump Database Row when a match is found") do |v|  
    options.rowdump = v;  
   end  
   
   opts.on("-m", "--metadata", "Look for search strings only in DB metadata (table and column names)") do |v|  
    options.metadata = v;  
   end  
   
   opts.on("-v", "--verbose", "Verbose output") do |v|  
    options.verbose = v;  
   end  
   
   opts.on_tail("-h", "--help", "Show this message") do  
    puts opts  
    exit  
   end  
   
   end  
   
   opts.parse!(args)  
    options  
   end# parse()  
   
 end# class CmdLineOptions  
   
 options = nil  
   
 begin  
  options = CmdLineOptions.parse(ARGV)  
 rescue (OptionParser::InvalidOption)  
  $stderr.puts "[-] Invalid option "  
  options = CmdLineOptions.parse(ARGV+["-h"])  
 end  
   
 if(options.dbs.length == 0)  
  $stderr.puts "[-] No Database available. Exiting !!"  
  exit  
 end  
   
 dbs = []  
 options.dbs.uniq!  
   
 dbs = options.dbs.collect do |db|  
  begin  
   throw Errno::ENOENT unless(File.file?(db))  
   Sequel.sqlite(db).tables  
   db  
  rescue  
   $stderr.puts "[-] \"#{db}\" is not a sqlite database"  
   nil  
  end  
 end  
   
 options.dbs = dbs.compact  
   
 if(options.dbs.length == 0)  
  $stderr.puts "[-] No Database available. Exiting."  
  exit  
 end  
   
 options.sstrings.uniq!  
   
 if(options.show_schema)  
   
  puts  
  puts "+"*80  
  puts "Database Schemas"  
  puts "+"*80  
   
  options.dbs.each do |db|  
   puts  
   puts "[DATABASE] #{db}"  
   Sequel.sqlite(db) do |dbhandle|  
    dbhandle.tables.each do |table|  
     puts "\t[TABLE] #{table}"  
     puts "\t\t[COLUMNS] #{dbhandle[table.to_sym].columns.join(', ')}"  
    end  
   end  
  end  
   
  puts "-"*80  
   
 end  
   
 regex_strings = []  
 regex_strings = options.sstrings.collect do |search|  
   
  regexstr = ""  
  regex = nil  
  if(options.exact)  
   regexstr = "^#{search}$"  
  else  
   regexstr = "#{search}"  
  end  
   
  if(options.case_sensitive)  
   regex = Regexp.new("#{regexstr}")  
  else  
   regex = Regexp.new("#{regexstr}", Regexp::IGNORECASE)  
  end  
  regex  
   
 end  
   
 options.sstrings = regex_strings  
   
 options.dbs.each do |database|  
   
  if(options.verbose)  
   puts  
   puts "+"*80  
   puts "Analyzing Database '#{database}'"  
   puts "+"*80  
  end  
   
  Sequel.sqlite(database) do |databasehandle|  
   databasehandle.tables.each do |table|  
    if(options.verbose)  
     puts  
     puts "-"*80  
     puts "Analyzing Table '#{table}'"  
     puts "-"*80  
    end  
   
    options.sstrings.each do |regex|  
     if(regex.match(table.to_s))  
      puts "[+] Table Name Match Found -> Database '#{database}' -> TABLE '#{table}'"  
     end  
    end  
   
    #Column Name Search  
    databasehandle[table.to_sym].columns.each do |column_name|  
     options.sstrings.each do |regex|  
      if(regex.match(column_name.to_s))  
       puts "[+] Column Name Match Found -> Database '#{database}' -> TABLE '#{table}' -> COLUMN '#{column_name}'"  
      end  
     end  
    end  
   
    #Data Search  
    if(options.sstrings.length > 0 && !options.metadata)  
     row = 0  
     databasehandle[table].each do |rowHash|  
      row = row + 1  
      rowHash.each do |key, value|  
       options.sstrings.each do |regex|  
        if(regex.match(value.to_s))  
         puts "[+] Data Match Found -> Database '#{database}' -> TABLE '#{table}', COLUMN '#{key}' -> ROW '#{row}'"  
         puts "\t[*] Row Dump\t=>\t#{rowHash.values.join('|')}" if(options.rowdump)  
        end  
       end  
      end  
     end  
    end  
   end  
  end  
 end  

Sunday, December 18, 2011

Decoding BigIP Cookie

BigIP cookie contains internal network IP and port information in encoded format. When decoded, these cookies can help create an internal network map with potential web server IPs and their ports.

F5 has described the encoding algorithm here. It works like this:
  1. If the IP address is a.b.c.d, it is encoded as d*256^3 + c*256^2 + b*256 +a
  2. To encode the port is to take the two bytes that store the port and reverse them. Thus, port 80 becomes 80 * 256 + 0 = 20480. Port 1433 (instead of 5 * 256 + 153) becomes 153 * 256 + 5 = 39173.
  3. These values are combined into cookie as <Encoded IP Address>.<Encoded Port Address>.<Componenet we are not concerned about>
These decoding mechanisms are packed into the following ruby script:
#!/usr/bin/ruby
#Cookie: BIGipcookie => 404007104.20480.0000
#Cookie: BIGipcookie => 404007104.39173.0000
if (ARGV.length == 0)
  $stderr.puts "No input provided. Run as \n\tbigip.rb BigIP Cookie Value"
  exit
end
ips = ARGV[0].split(".") 
encoded_val = ips[0].to_i
port_val = ips[1].to_i
ip = []
port = []
4.times do
  ip << encoded_val%256
  encoded_val /= 256
end
2.times do 
  port << port_val%256
  port_val /= 256
end
puts "IP Address : #{ip.join(".")}"
puts "Port       : #{port[0]*256 + port[1]}"

A Sample bigip.rb run

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.

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.