一覧SP-01-4 · WAF:HTTPの検査と制御

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

1
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']}
2
value = "' OR 1=1 --" query = urllib.parse.urlencode({"q":value}) print(request("direct","/vulnerable?"+query))
{'status': 200, 'origin': True, 'body': ['alice', 'bob', "O'Reilly"]}
3
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, '')]}
[]
4
normal = urllib.parse.urlencode({"q":"O'Reilly"}) print(request("baseline","/vulnerable?"+normal)["status"])
403
5
print(request("direct","/vulnerable?"+query)) print(len(observed()))
{'status': 200, 'origin': True, 'body': ['alice', 'bob', "O'Reilly"]}
1
6
print(request("direct","/fixed?"+query)) print(request("direct","/fixed?"+normal))
{'status': 200, 'origin': True, 'body': []}
{'status': 200, 'origin': True, 'body': ["O'Reilly"]}
7
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 --" []

演習完了