はじめに
技術部の桜です。
今回は、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 8259 (The JavaScript Object Notation (JSON) Data Interchange Format #Objects)より引用」
The names within an object SHOULD be unique.
RFCには、「name部分は基本的に一意であるべき」と言った事が記載されていると、私は解釈しています。
では、キー (name)が2つ存在する時、アプリケーションは後ろのキーの値を処理に使用するのにも関わらず、WAFは先頭のキーもしくは値を検査するのみで、後ろのキーは検査しない、といった挙動が起きないかを検証してみます。12
ここでは、以下2つの状況を検証します。
- 後ろのキーの値に、WAFによって検知されるペイロードを持つ場合
- 前のキーの値に、WAFによって検知されるペイロードを持つ場合
1. 後ろのキーの値に、WAFによって検知されるペイロードを持つリクエスト
2. 前のキーの値に、WAFによって検知されるペイロードを持つリクエスト
結果は、どちらもWAFに検知されていることがわかります。
キーが2つ存在する時、WAFは前・後のみを検査するという事はないため、どちらかに無害な値を入れておくことで、WAFをバイパスできるということはなさそうです。
BOMをJSON文字列の開始タグの前につける
- 「RFC 8259 (The JavaScript Object Notation (JSON) Data Interchange Format # Character Encoding)より引用
Implementations MUST NOT add a byte order mark (U+FEFF) to the beginning of a networked-transmitted JSON text.
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でエンコードを行う
- 「RFC 8259 (The JavaScript Object Notation (JSON) Data Interchange Format #Strings)より引用」
All Unicode characters may be placed within the quotation marks, except for the characters that MUST be escaped: quotation mark, reverse solidus, and the control characters (U+0000 through U+001F).
Unicodeを使用したバイパスを行います。3
エンコードはCyberChefを使って行います。
-- エンコード 前
<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
を追加して、Hackvertor
のgzip_compressタグ
でペイロードを囲います。
ここまで出来たらリクエストを送信しましょう。
バイパスが出来ていそうです。4
検証: deflateを用いてWAFをバイパスする
検証: gzipを用いてWAFをバイパスすると基本的に手順は一緒ですが、以下の2点を変更してください。
- Content-Encodingをdeflateにする。
- Hackvertorで、
deflate_compressタグ
を使用する。
こちらもバイパス出来ていそうです。
パースの実装依存を利用する
JSON文字列のパースの結果は言語によって違う部分があります。
例えば、rubyのJSON.parse()
は、\s
をs
として解釈をします。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をバイパス出来るという結果になりました。
最後に
弊社では、今回のように診断技術を向上させるための検証を行っています。
脆弱性診断を行う予定がございましたら、ホームページよりお問い合わせが可能ですので、ぜひ一度弊社にお声掛けいただけますと幸いです。
-
AWS WAF Bypass: invalid JSON object and unicode escape sequences #JSON duplicate keysにて、既に紹介されています。 ↩︎
-
SECCON Beginners CTF 2021【Web】json 作問者writeupでも取り上げられています。 ↩︎
-
AWS WAF Bypass: invalid JSON object and unicode escape sequencesによって紹介されています。 ↩︎
-
CVE-2021-45468: Imperva Cloud WAFにおいて、
Content-Encoding: gzip
を使用したHTTP POSTでWAFがリクエストを評価しないといったことが起きていたようです。 ↩︎ -
Flatt Security Developers' Quiz #6で解法の一例として紹介されています。 ↩︎