sqlmap は SQL インジェクションに特化したオープンソースのペネトレーションテストツール。 これを使うと Web アプリケーションの特定のパラメータに SQL インジェクションの脆弱性があるか否かを確認しやすい。
注意: 外部のサーバに使うことは攻撃となるので絶対にしないように。
今回使った環境は次の通り。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.12.6 BuildVersion: 16G29 $ sqlmap --version 1.1.8#stable $ python --version Python 2.7.10
インストール
sqlmap は Homebrew でインストールできる。
$ brew install sqlmap
これで sqlmap
コマンドが使えるようになる。
$ sqlmap --version 1.1.8#stable
ちなみに sqlmap は Python で書かれているけど Python 3 では動かない。
$ python --version Python 3.6.2 $ sqlmap [CRITICAL] incompatible Python version detected ('3.6.2'). For successfully running sqlmap you'll have to use version 2.6.x or 2.7.x (visit 'http://www.python.org/download/')
作者の方針的に Python 3 対応をする予定はないようだ。
Port code to Python 3 · Issue #93 · sqlmapproject/sqlmap · GitHub
ターゲット
今回はツールの検証のために意図的に SQL インジェクションの脆弱性を仕込んだ Web アプリケーションを用意してみた。 これは Python の Flask で書いてある。 データベースには組み込みで使える SQLite3 を使った。
#!/usr/bin/env python # -*- coding: utf-8 -*- import sqlite3 from flask import Flask from flask import request from flask import abort from flask import jsonify app = Flask(__name__) @app.route('/users') def get(): # クエリパラメータから 'name' の値を取得する name = request.args.get('name') # ひとまずクエリパラメータは必須ということで if name is None: abort(400) conn = sqlite3.connect('users.db') cursor = conn.cursor() try: # XXX: クエリの組み立てにプレースホルダーを使っておらず SQL インジェクションが発生しうる query = 'SELECT * FROM users where name like \'{name}\''.format(name=name) # noqa cursor.execute(query) # 結果を取得して表示する result = cursor.fetchall() finally: # 後片付け cursor.close() conn.close() # レスポンスの組み立て response_body = { 'users': [ { 'name': name, 'age': age, } for name, age in result ] } response = jsonify(response_body) response.status_code = 200 response.headers['Content-Type'] = 'application/json' return response def initialize_db(): conn = sqlite3.connect('users.db') cursor = conn.cursor() try: # 既存の内容があれば消す cursor.execute('DROP TABLE IF EXISTS users') # テーブルのスキーマを作る cursor.execute('CREATE TABLE users (name TEXT, age INTEGER)') # ダミーデータを追加する cursor.execute('INSERT INTO users VALUES (?, ?)', ('Alice', 20)) cursor.execute('INSERT INTO users VALUES (?, ?)', ('Bob', 25)) cursor.execute('INSERT INTO users VALUES (?, ?)', ('Carol', 30)) conn.commit() finally: # 後片付け cursor.close() conn.close() def main(): # データベースを初期化する initialize_db() # アプリケーションを起動する app.run() if __name__ == '__main__': main()
上記のアプリケーションは SQL の実行にプレースホルダを使っていないため SQL インジェクションの脆弱性がある。
使い方
動かすには Flask が必要なのでインストールする。
$ pip install flask
あとは先ほどのファイルを適当な名前で保存して実行するだけ。
$ python queryparam.py * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
上手くいけば次のように URL のクエリパラメータで既存のユーザを検索できるアプリケーションが動く。
ただ、このパラメータ name
には SQL インジェクションの脆弱性がある。
$ curl 'http://localhost:5000/users?name=Alice' { "users": [ { "age": 20, "name": "Alice" } ] }
sqlmap でペネトレーションテストする
それでは sqlmap を使って先ほどのアプリケーションをペネトレーションテストしてみよう。
クエリパラメータであれば -u
オプションを使って URL をそのまま指定するだけでテストできる。
--dump
オプションは脆弱性があったときにデータベースの内容をダンプすることを意味する。
$ sqlmap -u 'http://localhost:5000/users?name=Alice' --dump
いくつか質問に答えていって、脆弱性が実際に見つかると次のような出力が得られる。
$ sqlmap -u 'http://localhost:5000/users?name=Alice' --dump ...(snip)... GET parameter 'name' is vulnerable. Do you want to keep testing the others (if any)? [y/N]
さらに進めると users
テーブルの内容が表示される。
$ sqlmap -u 'http://localhost:5000/users?name=Alice' --dump ...(snip)... GET parameter 'name' is vulnerable. Do you want to keep testing the others (if any)? [y/N] ...(snip)... Database: SQLite_masterdb Table: users [3 entries] +-----+-------+ | age | name | +-----+-------+ | 20 | Alice | | 25 | Bob | | 30 | Carol | +-----+-------+
仮にこれが実際の攻撃ならデータベースの内容を盗み出されていたことになる。
フォームタグ内のパラメータをテストする
さっきはクエリパラメータだったけど、次はフォームタグで送信される内容を想定してみよう。
これはようするに Content-Type
が application/x-www-form-urlencoded
されるもの。
サンプルコードは次の通り。 先ほどと同じようにユーザを検索するというもの。
#!/usr/bin/env python # -*- coding: utf-8 -*- import sqlite3 from flask import Flask from flask import request from flask import abort from flask import jsonify app = Flask(__name__) @app.route('/users', methods=['POST']) def post(): # フォームパラメータから 'name' の値を取得する name = request.form.get('name') # ひとまずクエリパラメータは必須ということで if name is None: abort(400) conn = sqlite3.connect('users.db') cursor = conn.cursor() try: # XXX: クエリの組み立てにプレースホルダーを使っておらず SQL インジェクションが発生しうる query = 'SELECT * FROM users where name like \'{name}\''.format(name=name) # noqa cursor.execute(query) # 結果を取得して表示する result = cursor.fetchall() finally: # 後片付け cursor.close() conn.close() # レスポンスの組み立て response_body = { 'users': [ { 'name': name, 'age': age, } for name, age in result ] } response = jsonify(response_body) response.status_code = 200 response.headers['Content-Type'] = 'application/json' return response def initialize_db(): conn = sqlite3.connect('users.db') cursor = conn.cursor() try: # 既存の内容があれば消す cursor.execute('DROP TABLE IF EXISTS users') # テーブルのスキーマを作る cursor.execute('CREATE TABLE users (name TEXT, age INTEGER)') # ダミーデータを追加する cursor.execute('INSERT INTO users VALUES (?, ?)', ('Alice', 20)) cursor.execute('INSERT INTO users VALUES (?, ?)', ('Bob', 25)) cursor.execute('INSERT INTO users VALUES (?, ?)', ('Carol', 30)) conn.commit() finally: # 後片付け cursor.close() conn.close() def main(): # データベースを初期化する initialize_db() # アプリケーションを起動する app.run() if __name__ == '__main__': main()
先ほどと同じように適当な名前をつけて実行する。
$ python contentbody.py * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
curl
コマンドからは次のようにして呼び出せる。
$ curl -X POST -d 'name=Alice' http://localhost:5000/users { "users": [ { "age": 20, "name": "Alice" } ] }
sqlmap
コマンドで上記のパラメータをテストするときは --data
オプションを使う。
$ sqlmap -u 'http://localhost:5000/users' --data 'name=Alice' --dump
実行すると次のようにデータベースの内容がダンプして得られる。
$ sqlmap -u 'http://localhost:5000/users' --data 'name=Alice' --dump ...(snip)... Database: SQLite_masterdb Table: users [3 entries] +-----+-------+ | age | name | +-----+-------+ | 20 | Alice | | 25 | Bob | | 30 | Carol | +-----+-------+
JSON を使った WebAPI をテストする
次は Content-Type
が application/json
な WebAPI をテストするときについて。
サンプルコードは次の通り。
WebAPI なのに POST でユーザを検索するっていう、ちょっと不自然なものになっちゃってる。
これは INSERT
とか UPDATE
を使った SQL インジェクションは難しくて sqlmap がそれに対応していないのが理由。
#!/usr/bin/env python # -*- coding: utf-8 -*- import sqlite3 from flask import Flask from flask import request from flask import abort from flask import jsonify app = Flask(__name__) @app.route('/users', methods=['POST']) def post(): # クエリパラメータから 'name' の値を取得する name = request.json.get('name') # ひとまずクエリパラメータは必須ということで if name is None: abort(400) conn = sqlite3.connect('users.db') cursor = conn.cursor() try: # XXX: クエリの組み立てにプレースホルダーを使っておらず SQL インジェクションが発生しうる query = 'SELECT * FROM users where name like \'{name}\''.format(name=name) # noqa cursor.execute(query) # 結果を取得して表示する result = cursor.fetchall() finally: # 後片付け cursor.close() conn.close() # レスポンスの組み立て response_body = { 'users': [ { 'name': name, 'age': age, } for name, age in result ] } response = jsonify(response_body) response.status_code = 200 response.headers['Content-Type'] = 'application/json' return response def initialize_db(): conn = sqlite3.connect('users.db') cursor = conn.cursor() try: # 既存の内容があれば消す cursor.execute('DROP TABLE IF EXISTS users') # テーブルのスキーマを作る cursor.execute('CREATE TABLE users (name TEXT, age INTEGER)') # ダミーデータを追加する cursor.execute('INSERT INTO users VALUES (?, ?)', ('Alice', 20)) cursor.execute('INSERT INTO users VALUES (?, ?)', ('Bob', 25)) cursor.execute('INSERT INTO users VALUES (?, ?)', ('Carol', 30)) conn.commit() finally: # 後片付け cursor.close() conn.close() def main(): # データベースを初期化する initialize_db() # アプリケーションを起動する app.run() if __name__ == '__main__': main()
上記のアプリケーションは curl
コマンドからだと次のように呼び出せる。
$ curl -X POST -H "Content-Type: application/json" -d '{"name": "Alice"}' http://localhost:5000/users { "users": [ { "age": 20, "name": "Alice" } ] }
上記を sqlmap でテストするときは、まず Content-Body に JSON を含んだリクエスト内容をファイルとして用意しておく。
$ cat << 'EOF' > json-request.txt POST /users HTTP/1.1 Host: localhost:5000 Content-Type: application/json {"name": "Alice"} EOF
sqlmap
コマンドでは、上記のファイルを -r
オプションで指定する。
$ sqlmap -r json-request.txt --dump
すると JSON 使ってる?って sqlmap が確認してくるので Y
する。
$ sqlmap -r json-request.txt --dump
JSON data found in POST data. Do you want to process it? [Y/n/q]
あとはこれまでと同じようにやっていくとデータベースの内容が出力された。
...(snip)... Database: SQLite_masterdb Table: users [3 entries] +-----+-------+ | age | name | +-----+-------+ | 20 | Alice | | 25 | Bob | | 30 | Carol | +-----+-------+
まとめ
今回は sqlmap を使った SQL インジェクションのペネトレーションテストをするやり方について書いた。 正直なところ SQL インジェクションくらい初歩的な脆弱性ならコードを確認した方が早いのではとも思ってしまう。 しかし、脆弱性の有無を調べるにはコードを確認するような静的解析以外にも、今回やったような動的解析もまた一つのやり方といえるだろう。 繰り返しになるけど sqlmap を外部のサーバに使うのは攻撃になるので絶対にやらないこと。
- 作者: 八木毅,村山純一,秋山満昭
- 出版社/メーカー: コロナ社
- 発売日: 2015/03/17
- メディア: 単行本
- この商品を含むブログを見る
スマートPythonプログラミング: Pythonのより良い書き方を学ぶ
- 作者: もみじあめ
- 発売日: 2016/03/12
- メディア: Kindle版
- この商品を含むブログ (1件) を見る