Azure で攻撃や検知のデモを簡単に行って、簡単に消せる環境を用意したいのでこの記事を書いています。OWASP Juice Shop のデモ環境を作ります。
OWASP Juice Shop とは OWASP Top 10 で有名な OWASP Foundation の有志によってメンテナンスされているプロジェクトで、ここのリソースを利用することで "洗練された" セキュアではないアプリケーション - OWASP Juice Shop をホストすることができます。OWASP Top 10 など脅威を学習するための題材として役に立ち、文書を読むだけでは腑に落ちない脅威を実際に手を動かして試してみることができますし、(自分で探す必要がありますが)スコア機能もあるのでみんなで脆弱性探し競争をすることもできます。
Node.js で動くのでローカル コンピューターや任意の VM などにインストールすることもできますが、今回はプロジェクトで提供されている docker イメージを利用して App Service の上のコンテナとして OWASP Juice Shop を動かします。
OWASP Juice Shop の環境の構築や遊び方の詳細は Pwning OWASP Juice Shop を参照してください。
目次
- Web App for Containers で OWASP Juice Shop を展開する
- SQL インジェクションで攻撃してみる (ここまですぐです)
- Application Gateway の WAF を置いてみる(ここからが長いです)
- どうなったか見てみる
Web App for Containers で OWASP Juice Shop を展開する
さっそく OWASP Juice Shop を展開します。単に攻撃のデモを行うことが目的であればこの節だけで OK です。
Azure Portal の右上からクラウド シェルで次のコマンドを実行します。PowerShell と Bash 、どっちでも大丈夫だと思います。
az group create --name <リソースグループ名> --location "Japan East"
az appservice plan create --name <App Service プランの名前> --resource-group <リソースグループ名> --sku B1 --is-linux
az webapp create -g <リソースグループ名> -p <App Service プランの名前> -n <ユニークなアプリケーションの名前> -i bkimminich/juice-shop
一連のコマンドは東日本リージョンに B1 (価格はこちら)の App Service プランを作ります。
3 つめのコマンドの引数 bkimminich/juice-shop
が OWASP Juice Shop の docker イメージの名前で、コマンドが完了してしばらくすると http(s)://<ユニークなアプリケーションの名前>.azurewebsites.net
という URL で OWASP Juice Shop にアクセスすることができます。こんな画面が表示されれば展開は完了です。
とりあえず SQL インジェクションしてみる
データベースに対してクエリを投げる場所があれば (そして一般的なデータベース セキュリティのプラクティスに従っていなければ) SQL インジェクションの脆弱性が存在する可能性があり、独自に実装されたユーザー認証はデータベースにクエリを投げる可能性があります。
脆弱なアプリケーションは大きすぎる入力や特殊文字に弱いので画面右上の Account
からユーザー認証に特殊文字を投げ込んでみます。シングル クォートとダブル クォートはとりあえず含めてみましょう。
認証は失敗しましたが少し様子が変です。ブラウザの F12 で開発者ツールを開き、Network
を見てみましょう。login に 500 エラーが返されていて、Response
を展開するとサーバー側で発生したエラーがそのままクライアントに返されてしまっています。
ちなみにこのまずいエラー処理自体がセキュリティのプラクティスに従っていない脆弱性なので、画面に脆弱性見つけたねおめでとう [You successfully sloved achallenge: Error Handling ...] が表示されます。
{
"error": {
"message": "SQLITE_ERROR: unrecognized token: \"\"%$*&' AND password = '6d0ad9a207df8c02effc765379e3972e' AND deletedAt IS NULL\"",
"stack": "Error\n at Database.<anonymous> (/juice-shop/node_modules/sequelize/lib/dialects/sqlite/query.js:179:27)\n at Database.serialize (<anonymous>)\n at /juice-shop/node_modules/sequelize/lib/dialects/sqlite/query.js:177:50\n at new Promise (<anonymous>)\n at Query.run (/juice-shop/node_modules/sequelize/lib/dialects/sqlite/query.js:177:12)\n at /juice-shop/node_modules/sequelize/lib/sequelize.js:314:28\n at processTicksAndRejections (node:internal/process/task_queues:96:5)",
"name": "SequelizeDatabaseError",
"parent": {
"errno": 1,
"code": "SQLITE_ERROR",
"sql": "SELECT * FROM Users WHERE email = ''\"%$*&' AND password = '6d0ad9a207df8c02effc765379e3972e' AND deletedAt IS NULL"
},
"original": {
"errno": 1,
"code": "SQLITE_ERROR",
"sql": "SELECT * FROM Users WHERE email = ''\"%$*&' AND password = '6d0ad9a207df8c02effc765379e3972e' AND deletedAt IS NULL"
},
"sql": "SELECT * FROM Users WHERE email = ''\"%$*&' AND password = '6d0ad9a207df8c02effc765379e3972e' AND deletedAt IS NULL",
"parameters": {}
}
}
SQLITE_ERROR ということなのでデータベースは SQLITE を使っていて、 SELECT * FROM Users WHERE email = ''\"%$*&' AND ...
というクエリの実行でちゃんと解析することができずにエラーになったようです。ネットで調べてみると SQLite のコメントは --
なので WHERE 以下が True になるようにユーザー名
を渡してあげましょう。たとえば次のようなクエリが目標です。
SELECT * FROM Users WHERE email = '' or 1=1 --
' AND password = ...
無事ログインすることができたと思います。クエリでユーザーが特定されずにログインに成功すると管理者ユーザーでログインできてしまうという、なかなかすごい作りになっています。
Application Gateway の WAF を置いてみる
OWASP Juice Shop を攻撃することができたので、次は攻撃を防いでみます。ここからの作業は手順にすると少し長くて何回もやるのはけっこう大変なので Bicep でコード化します。learn のクイックスタート と Bicep に関する Learn モジュール を終えておくと理解が進みやすく、今後も応用がききます。
最低限必要なツールはこれです。↑ のクイックスタートでもカバーされています。
- Visual Studio Code
- Azure CLI / PowerShell
- Bicep と Azure Account の拡張機能
Bicep で OWASP Juice Shop を展開する
最初に Visual Studio Code で適当なフォルダを開き、main.bicep というファイルを作ってください。こんなコードを書きます。
resource JuicePlan 'Microsoft.Web/serverfarms@2020-12-01' = {
name: 'plan-juice'
location: resourceGroup().location
kind: 'linux'
sku: {
name: 'B1'
capacity: 1
}
properties:{
reserved:true
}
}
resource JuiceApp 'Microsoft.Web/sites@2021-01-15' = {
name: '<ユニークなアプリケーションの名前>'
location: resourceGroup().location
properties: {
siteConfig:{
linuxFxVersion: 'DOCKER|bkimminich/juice-shop'
}
serverFarmId: JuicePlan.id
}
}
name 属性は任意に書き換えてください。Azure 上に作成されるリソースの名前になります。App Service Plan (Microsoft.Web/serverfarms リソース) の properties 属性で reserved:true を指定しないと windows のプランが作られてしまうので注意してください。
Visual Studio Code のエクスプローラーで main.bicep を開き、Deploy Bicep File...
を選ぶとウィンドの上の方に展開に必要な入力が求められるので適切なものを入力します。
- deployment name - 任意
- subscription - 任意
- resource group - 新規に作成するか既存の物を選択
- location - Japan East を選択
- parameter file - None を選択
しばらくするとhttp(s)://<ユニークなアプリケーションの名前>.azurewebsites.net
という URL で OWASP Juice Shop にアクセスすることができます。コマンドから作成した時と同じアプリケーションができていると思います。
Application Gateway を展開する
ここは依存関係もありなかなか複雑で、次のことを行います
- Application Gateway を配置するための仮想ネットワークの作成
- Application Gateway が使うするパブリック IP アドレスの作成と FQDN の割り当て
- WAFv2 の Application Gateway の作成
- リスナーの構成
- App Service をバックエンドに割り当て
- リスナーとバックエンドをつなぐ構成
先ほどの main.bicep ファイルに以下のコードを追記します。
基本的な構成はApplication Gateway のドキュメントに従っていますが、バックエンドに App Service を指定するため少し構成が違うという点にご注意ください。backendHttpSettingsCollection
属性は pickHostNameFromBackendAddress:true
を設定しないと動かないと思います。
他にも WAF の設定を行うために追加した設定でmaxRequestBodySizeInKb
を 128kb に設定していたり、逆に元のコードから引き継いだ maxCapacity
によって最大 10 インスタンスまでスケールするようになっていたりするので、作成されたリソースがちゃんとしてるかどうか(あと課金が大丈夫か)は確認してください。
var applicationGateWayName = 'appwg-juice'
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2019-11-01' = {
name: 'vnet-juiceapp'
location: resourceGroup().location
properties: {
addressSpace: {
addressPrefixes: [
'10.33.0.0/16'
]
}
subnets: [
{
name: 'ApplicaionGateway'
properties: {
addressPrefix: '10.33.1.0/24'
}
}
{
name: 'Apps'
properties: {
addressPrefix: '10.33.3.0/24'
}
}
]
}
}
resource publicIPAddress 'Microsoft.Network/publicIPAddresses@2021-05-01' = {
name: 'pip-appgw-juice'
location: resourceGroup().location
sku: {
name: 'Standard'
}
properties: {
publicIPAddressVersion: 'IPv4'
publicIPAllocationMethod: 'Static'
idleTimeoutInMinutes: 4
dnsSettings:{
domainNameLabel: JuiceApp.name
}
}
}
resource applicationGateWay 'Microsoft.Network/applicationGateways@2021-05-01' = {
name: applicationGateWayName
location: resourceGroup().location
properties: {
sku: {
name: 'WAF_v2'
tier: 'WAF_v2'
}
gatewayIPConfigurations: [
{
name: 'appGatewayIpConfig'
properties: {
subnet: {
id: resourceId('Microsoft.Network/virtualNetworks/subnets', virtualNetwork.name, 'ApplicaionGateway')
}
}
}
]
frontendIPConfigurations: [
{
name: 'appGwPublicFrontendIp'
properties: {
privateIPAllocationMethod: 'Dynamic'
publicIPAddress: {
id: resourceId('Microsoft.Network/publicIPAddresses', publicIPAddress.name)
}
}
}
]
frontendPorts: [
{
name: 'port_80'
properties: {
port: 80
}
}
]
backendAddressPools: [
{
name: 'myBackendPool'
properties: {
backendAddresses:[
{
fqdn: '${JuiceApp.name}.azurewebsites.net'
}
]
}
}
]
backendHttpSettingsCollection: [
{
name: 'myHTTPSetting'
properties: {
port: 80
protocol: 'Http'
cookieBasedAffinity: 'Disabled'
pickHostNameFromBackendAddress: true
}
}
]
httpListeners: [
{
name: 'myListener'
properties: {
frontendIPConfiguration: {
id: resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations', applicationGateWayName, 'appGwPublicFrontendIp')
}
frontendPort: {
id: resourceId('Microsoft.Network/applicationGateways/frontendPorts', applicationGateWayName, 'port_80')
}
protocol: 'Http'
requireServerNameIndication: false
}
}
]
requestRoutingRules: [
{
name: 'myRoutingRule'
properties: {
ruleType: 'Basic'
httpListener: {
id: resourceId('Microsoft.Network/applicationGateways/httpListeners', applicationGateWayName, 'myListener')
}
backendAddressPool: {
id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', applicationGateWayName, 'myBackendPool')
}
backendHttpSettings: {
id: resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', applicationGateWayName, 'myHTTPSetting')
}
}
}
]
webApplicationFirewallConfiguration: {
enabled: true
firewallMode: 'Prevention'
ruleSetType: 'OWASP'
ruleSetVersion: '3.2'
requestBodyCheck: true
maxRequestBodySizeInKb: 128
}
enableHttp2: false
autoscaleConfiguration: {
minCapacity: 0
maxCapacity: 10
}
}
dependsOn: [
virtualNetwork
publicIPAddress
]
}
先ほどと同じく main.bicep を右クリックし、Deploy Bicep File...
からデプロイします。Application Gateway は展開に少し時間がかかるので、5分ほどすると準備が整うと思います。
今回の手順では特に制限はしていないため、http(s)://<ユニークなアプリケーションの名前>.azurewebsites.net
で直接 Web App for Container 上の OWASP Juice Shop にアクセスすることができます。これは Web App for Container 単体でのデプロイと同じです。
ただし、今回は Application Gateway のためにパブリック IP アドレスを作成し、FQDN を割り当てています。
http(s)://<ユニークなアプリケーションの名前>.japaneast.cloudapp.azure.com
で表示されるのが Application Gateway 経由でのアクセスとなり、この通信は WAF で保護されます。
先ほどの SQL インジェクションを試すためにユーザー名 ' or 1=1 --
でアクセスしてみると...
元のアプリケーション作りがそうなっているのでエラーの表示がちょっと変ですが、ログインは無事に失敗し、SQL インジェクションがブロックされています。
今後 Application Gateway のログで攻撃を検出する方法を追記したいと思います。