WAFの検知結果とアプリケーションの修正
検知した値と影響範囲を調べ、入口での対策とアプリ内部の修正を別々に検証します。
説明
WAFの仮想パッチは、アプリを直す前に特定の入力を入口で止める対策です。入口に別経路があれば届かず、粗い条件では正当な入力も遮断する場合があります。
公開の三人だけを持つ検索アプリで、SQLを文字列連結した処理とパラメーター化した処理を比較します。WAFで引用符を止める暫定ルール、正常な引用符入りの名前、恒久修正後の検索結果まで確認します。
検知した値と影響範囲を調べ、入口での対策とアプリ内部の修正を別々に検証します。
1通常の検索結果を確認する
公開試料にはalice、bob、O'Reillyの三人を保存しています。/vulnerableは文字列連結、/fixedは値束縛です。
from sp01 import *
reset()
print(request("direct","/vulnerable?q=alice"連結したSQLで通常値を検索する))
print(request("direct","/fixed?q=alice"値束縛で同じ検索を行う))どちらもalice一人です。通常値だけでは両実装の違いが見えません。
2条件の構造を変える固定値を渡す
固定のOR条件を含む値をURL符号化します。保存データは公開試料だけです。
value = "' OR 1=1 --"
query = urllib.parse.urlencode固定検索値をクエリーへ符号化する({"q":value})
print(request("direct","/vulnerable?"+query同じ連結SQLへ送る))連結したSQLでは条件が全件に広がり、三人が返ります。入力が名前の値に留まらず、WHERE句の構造へ入りました。
3WAFの暫定ルールで入口を止める
同じ要求をbaselineへ送ります。
reset()
print(request("baseline"引用符検査のある入口へ送る,"/vulnerable?"+query))
print(summary())
print(observed()後段処理が始まっていないことを確認する)403でid1100が一致し、アプリの記録は空です。監査のresponse.statusが未設定になる遮断位置もあるため、実HTTP応答とアプリ到達を併せて照合します。
4暫定ルールによる正当な入力の影響を調べる
引用符を含む正常名O'Reillyも同じ条件へ一致します。
normal = urllib.parse.urlencode({"q":"O'Reilly"正当な引用符入りの名前を用意する})
print(request("baseline","/vulnerable?"+normal)["status"])403です。引用符の有無だけでは正当な名前とSQL構造の変更を判別できず、機能上の影響があります。
5WAFを通らない経路で残存影響を確認する
同じ未修正処理へdirect経路で再度送ります。
print(request("direct"入口の検査を通らない経路を比較する,"/vulnerable?"+query))
print(len(observed()))三人が返り、後段にも到達します。WAFで一経路を遮断しても、アプリ内部の連結処理は変わっていません。
6パラメーター化した処理へ同じ値を渡す
/fixedはSELECTの条件値をplaceholderで束縛します。
print(request("direct","/fixed?"+query修正済み検索へ同じ固定値を渡す))
print(request("direct","/fixed?"+normal正常な引用符入りの名前も試す))OR条件を含む値は該当名0件、O'Reillyは1件です。構造を変える入力は値として扱われ、通常の名前を許せるようになりました。
7修正済み経路で回帰ケースを実行する
通常名、存在しない名前、引用符入り名、固定OR条件をまとめて比較します。
for name in修正後の検索結果を回帰ケースで照合する ("alice","nobody","O'Reilly",value):
target = "/fixed?"+urllib.parse.urlencode({"q":name})
print(repr(name),request("baseline",target)["body"])修正後はaliceとO'Reillyだけが各1件、ほかは0件です。暫定ルールは/vulnerableに限定しているため、修正済み経路の通常入力を妨げません。
まとめ
検知ルールだけでなく、一致値と後段到達を確認します。
仮想パッチの正常系への影響と迂回経路を調べます。
アプリ内部を修正し、同じ入力群で回帰試験します。
操作履歴と解説を振り返れます。
Python HTTP client / Coraza 3.7.0 / coraza-caddy 2.5.0 / Caddy 2.11.4
from sp01 import *
reset()
print(request("direct","/vulnerable?q=alice"))
print(request("direct","/fixed?q=alice")){'status': 200, 'origin': True, 'body': ['alice']}
{'status': 200, 'origin': True, 'body': ['alice']}value = "' OR 1=1 --"
query = urllib.parse.urlencode({"q":value})
print(request("direct","/vulnerable?"+query)){'status': 200, 'origin': True, 'body': ['alice', 'bob', "O'Reilly"]}reset()
print(request("baseline","/vulnerable?"+query))
print(summary())
print(observed()){'status': 403, 'origin': False, 'body': ''}
{'uri': '/vulnerable?q=%27+OR+1%3D1+--', 'engine': 'On', 'interrupted': True, 'matches': [(1100, '')]}
[]normal = urllib.parse.urlencode({"q":"O'Reilly"})
print(request("baseline","/vulnerable?"+normal)["status"])403
print(request("direct","/vulnerable?"+query))
print(len(observed())){'status': 200, 'origin': True, 'body': ['alice', 'bob', "O'Reilly"]}
1print(request("direct","/fixed?"+query))
print(request("direct","/fixed?"+normal)){'status': 200, 'origin': True, 'body': []}
{'status': 200, 'origin': True, 'body': ["O'Reilly"]}for name in ("alice","nobody","O'Reilly",value):
target = "/fixed?"+urllib.parse.urlencode({"q":name})
print(repr(name),request("baseline",target)["body"])'alice' ['alice'] 'nobody' [] "O'Reilly" ["O'Reilly"] "' OR 1=1 --" []
演習完了