demandosigno

なんとか生きていけるように

やられアプリ BadTodo - 3.1 SQLインジェクション 認証の回避

前回。OWASP ZAPにてざっと自動検査を行った。
やられアプリ BadTodo - 2 ZAPでのスキャン - demandosigno

今回から、検出された脆弱性が本当に存在するかを手動でチェックしていく。

・SQLインジェクションの一例
www.youtube.com

最初の例として徳丸さんの動画通りSQLインジェクションを検証する。

ZAPの検査結果。

POST /logindo.phpuserid=admin%27+UNION+ALL+select+NULL+--+ と指定することでSQLインジェクション疑いがあるとのこと。

ブラウザ方で実際に試す。
IDにadmin' UNION ALL select NULL -- (--の前後にも半角スペースがある)パスワードは所定のまま→ログインボタンをクリック。

するとエラー文が表示される。「SQLSTATE[21000]: Cardinality violation: 1222 The used SELECT statements have a different number of columns」

IPAの「ウェブ健康診断仕様」p.9 を確認すると、SQLインジェクションの判定基準として

  • 『'』(シングルクォート 1 つ)を入力することで
  • エラーになる
  • レスポンスに DBMS 等が出力するエラーメッセージ(例:SQLException、Query failed 等)が表示された場合にエラーが発生したと判定します(※注 1)。
  • ※ 注 1 DBMS のエラーメッセージと判断する基準は以下の通りとします。
    ・ DBMS の製品名(Oracle、Microsoft SQL Server、IBM DB2、MySQL、PostgreSQL 等)の全て又は一部が表示される。
    ・ 他のエラーメッセージとは明らかに異質なメッセージ、例えば、通常のエラーメッセージが日本語であるのに対し、英語のメッセージになっている等。
    https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017319.pdf

とあります。今回注入したSQL文は"UNION ALL"を使用して既存のクエリに新しい結果を追加しデータベースから不正に情報を取得しようとしたものです。
上記の例の通り単純な「'」でも検証してみます。

こちらでは「MariaDB」とDBの製品名も表示されています。

パラメータpwdでも同様にSQLエラーが表示されるのですが、ZAPでは検出されていませんでした。履歴を確認するとpwdに対するインジェクションの試行自体ができていないように見えました。
スキャンポリシーの攻撃強度を上げることでpwdへの攻撃も行われ、レスポンスにエラーメッセージが出ているリクエストもあったのですが、それでもSQLインジェクションとは検出されていませんでした。なぜだろう……。

ユーザIDなしでのログイン

さらに判定基準二つ目の「『検索キー』と『検索キー'and'a'='a』の比較」も試してみます。

ログインでき、レスポンスが「検索キーのみと同じ結果」になりました。SQLクエリのログを見てみると

SELECT id, userid FROM users WHERE userid='test'and'a'='a'

となっています。userid='test'が真、かつ'a'='a'も真でこれ自体は無意味な条件ですが、正常なレスポンスと相違ないレスポンスが返ってきているということはSQL文が正しく解釈されているということであり、その他のSQL文が成り立つ可能性が示唆されます。

実際ユーザーID'or'a'='aとすると条件が常に真となるためパスワードの方が合っていればどのようなIDでもログインできます。(より簡単に'or'1でもOKです)

これらのことからPOST /logindo.phpのパラメータidにSQLインジェクションがあると判定します。

パスワードなしでのログイン

ユーザID'#と入力するとパスワードなしでログインできます。(今回はtestというユーザを登録済みの前提ですが、デフォルトで登録されているadminで試してもよいです)
ユーザID'--でも同様。--の後ろに半角スペースが必要)

コード(logindo.php)は

<?php
~(中略)~
 $sql = "SELECT id, userid FROM users WHERE userid='$userid'";
    $sth = $dbh->query($sql);
    $row = $sth->fetch(PDO::FETCH_ASSOC);
    $sth = null;
    if (! empty($row)) {
      $sql = "SELECT id, userid, super FROM users WHERE userid='$userid' AND pwd='$pwd'"; //SQL 文を生成
      $sth = $dbh->query($sql); //生成した SQL 文をデータベースに実行
      $row = $sth->fetch(PDO::FETCH_ASSOC); //実行した SQL 文の結果を $row 変数に格納
      if (! empty($row)) { //$row 変数が空でなければ次の行を実行
        $user = $app->new_user($row['id'], $userid, $row['super'], true);
        if ($autologin) {
          badsetcookie('AUTOLOGIN', serialize($user), time() + 3600 * 24 * 365);
        }
        header('Location: ' . $url . '?rnd=' . h($rnd) . '&' . $app->sid());
      } else {
        error_exit("パスワードが違います", true);
      }
    } else {
      error_exit("そのユーザーは登録されていません", true);
    }
~(後略)~

のため 8行目の "SELECT id, userid, super FROM users WHERE userid='$userid' AND pwd='$pwd'" の部分に userid='test'#' AND pwd='' と入り、その結果DBのクエリは

SELECT id, userid, super FROM users WHERE userid='test'#' AND pwd=''

となり ' でユーザ名の入力を終了し # で以降をコメントアウトしているためユーザIDのみで検索をかけ、存在する場合結果を $row に格納するため11行目以降の処理が進みログインされます。

ユーザID、パスワード双方不明のままログイン

二つをまとめると、ユーザID'OR'1、パスワード'OR'1でログインできます。
(ユーザID'OR'1'OR'1または'OR'1'#でパスワード無しなどでもOKです)

次のようにBadTodoDB(MySQL)のクエリログを参照することができます

badtodo-dbコンテナの /var/lib/mysql の query.log を見ます。

次回も引き続きSQLインジェクションについて、今度は「Todo 検索画面」を対象に見ていきます。
次回:やられアプリ BadTodo - 3.2 SQLインジェクション 非公開情報の漏洩 - demandosigno

/* -----codeの行番号----- */