Ruby on Rails 脆弱性解説 - CVE-2016-0752(後編)

前回の記事(前編)の続きです。
本稿(後編)では主にデバッガを使って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%>"&#x7D;&#x7D;
  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`%>"&#x7D;&#x7D;
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

おわりに

最近のWebアプリケーションフレームワークは巨大で設計思想も難しく、セキュリティエンジニア(テストエンジニア)には取っ付き難いものがあります。
しかしこうして実際に手を動かしながら動作を確認してみれば、何となく理解できる気になって来た!、と思って頂ければ幸いです。

今回は検証環境に Docker を使用してみました。
普段は rvm を使って環境を分けていますが、数が増えてくると環境を混ぜてしまったり、どのアプリがどの環境だったかわからなくなってしまったりしていたので、 Docker への移行を進めています。

検証作業が終わった後は docker commit で検証環境をそのまま保存し、 docker push してDockerイメージを共有するまでの流れを書きたかったのですが、本稿のようにdocker の volume オプションを使うとマウントしたボリュームがDockerイメージに保存できません。

社内の Docker レジストリ(GitLab)を使って検証環境をそのまま保存・共有することを考えていて、よい解決策が見つかったら、おまけ記事を書くかもしれません。

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