やられアプリ 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』の比較」も試してみます。(まずは正しいユーザIDとパスワードを使います)

ログインでき、レスポンスが「『検索キー』のみでログインした場合と同じ結果(『admin』で通常ログインした場合と相違ない画面)」になりました。SQLクエリのログを見てみると

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

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

実際ユーザーID'or'a'='aとすると条件が常に真となるためパスワードが存在する値であればIDの値は関係なく(今回はhoge)パスワードが一致するユーザーでログインできます。(より簡単に'or'1だけでもOKです)

念のため最初の条件のFalse admin' and 'a' = 'b も試しadmin' and 'a' = 'a と異なった結果になるかも確認しておきます。

異なりました。'a' = 'b'は「偽」であるためIDが見つからずログインできません。こちらでも正しくSQL文が機能していることになります。

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

補足' and 'a' = 'aを試せば十分で' and 'a' = 'bは蛇足ではと思われるかもしれませんが、稀に「'以降を切り飛ばす」「X文字までしか認識しない」「実はユーザ名admin' and 'a' = 'aが存在した」などの場合にチェック漏れとなります。この時に' and 'a' = 'bも試すと' and 'a' = 'aとの比較においてレスポンスが異ならない結果となりますので、SQL文が解釈されたわけではないためSQLインジェクションの問題ではないと気づけます。

注意!OR 'a'='a'OR 1=1の入力時は慎重に行ってください。この条件がDELETEUPDATEに対して引っかかると誤ってデータを消し飛ばしてしまう可能性があります。 「ウェブ健康診断仕様」の検査項目は'and'a'='aとなっていますし、脆弱性診断においてAND 'a'='a'での検査は禁止というルールになっているところもあります。
--#でのコメントアウトも、絞り込み条件が無効化されて全件検索になる同様の危険性があります。
検査で使うリクエスト例に関してはWebアプリケーション脆弱性診断ガイドライン 第1.2版(PDF)も参照してください。

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

ユーザID'#と入力するとパスワードなしでログインできます。(今回はtestというユーザを登録済みの前提ですが、デフォルトで登録されているadminで試してもよいです)
ユーザID';ユーザID'--でも同様。MySQLの場合--の後ろに半角スペースが必要)
(DBMSによって多少違いがあり、今回色々な例を#で試していますがこれはMySQLに対して使えるパターンのため基本的にはコメントアウトは--前後にスペースありの方で試した方が良いです)

コード(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です)
(WHERE に抽出条件を付けて検索するわけですが今回の場合WHERE TRUEと同じ意味になるため全レコードが引っかかり、この場合通常は先頭のレコード=今回はadmin でログインされます)

「こんにちは、'OR'1(管理者) さん」と表示されていますが、マイページを見に行くと admin でログインされていることが分かります。

(パスワード='or'a'='aでも同じ意味なのですが、現状はパスワードの文字数が6字までしか受け付けないため通りません BadTodo - 17 認証(パスワードの強度・ログアウト)

補足

OR, ANDに付ける条件として「1=1」や「'a'='a'」を例に使うことが多いですが、WAFでの対策で単純なものは弾かれます。しかしTRUEであればよいため「2=2」「2>1」「'hoge' LIKE 'hog%'」「2 BETWEEN 1 AND 3」「'ab'=concat('a','b')」などで通ることがあります。

ただ「ウェブアプリケーションに対する脆弱性試験」を行う場合にはWAFがあるとアプリケーションに対する検査が不十分になるため、検査期間中はWAFやファイヤーウォールを止めてもらいます。

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

今後も度々これを確認しながら進めますので参照方法を覚えておいてください。

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

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

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