Wizard-Gallery
The council’s top priority is to protect the flag, no matter the cost. Oh hey look, it’s a photo gallery. What could go wrong? Hint: RCE is a luxury nowadays.
Author: Ashray Shah
Challenge Description
They’ve given an image-upload service. The logo file is served separately at /logo.
There’s also a /logo-sm endpoint that uses ImageMagick to convert logo.png
to a smaller size (logo-sm.png) and serve that file.
Solution
Attempting File Validation Bypass (Red-Herring)
So obviously, the first thing I tried was to upload non-image files.
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'}
BLOCKED_EXTENSIONS = {'exe', 'jar', 'py', 'pyc', 'php', 'js', 'sh', 'bat', 'cmd',
'com', 'scr', 'vbs', 'pl', 'rb', 'go', 'rs', 'c', 'cpp', 'h'}
# --- snip ---
def allowed_file(filename):
if '.' not in filename:
return False
basename = os.path.basename(filename)
if '.' not in basename:
return False
extension = basename.rsplit('.', 1)[1].lower()
if extension in BLOCKED_EXTENSIONS:
return False
return extension in ALLOWED_EXTENSIONS
def is_blocked_extension(filename):
if '.' not in filename:
return False
basename = os.path.basename(filename)
if '.' not in basename:
return False
extension = basename.rsplit('.', 1)[1].lower()
return extension in BLOCKED_EXTENSIONSHmm… Seems like they’ve just validated filenames by extension string. They don’t check for actual MIME-type, but whatever.
Directory Traversal
Looking into the handler for /upload endpoint…
original_filename = file.filename
file_path = os.path.join(app.config['UPLOAD_FOLDER'], original_filename)
file.save(file_path)Yeah, that’s plain directory traversal right there.
I can manipulate the uploaded image name to something like ../../path/to/wherever.
Cool, that means I can overwrite the logo.png file at project root with anything.
CVE-2022-44268: Arbitrary File Read in ImageMagick
The ImageMagick version used in the app is vulnerable to Arbitrary File Read attack (CVE-2022-44268)
When ImageMagick performs operations such as resizing on a PNG file, it may include the content of a system file, given that the magick binary has the necessary permissions to read it. This vulnerability arises due to the mishandling of textual chunks within PNG files. A malicious actor can exploit this vulnerability by crafting a PNG file or using an existing one and adding a textual chunk type (tEXt). These chunks consist of a keyword and a text string. In this case, if the keyword matches the string “profile” (without quotes), ImageMagick will interpret the accompanying text string as a filename and attempt to load its content as a raw profile. As a result, when the resized image is downloaded, it will contain the content of the remote file specified by the attacker.
For more information, see this article from MetabaseQ.
Okay so, the convert command is vulnerable. Is it being used?
@app.route('/logo-sm.png')
def logo_small():
# A smaller images looks better on mobile so I just resize it and serve that
logo_sm_path = os.path.join(app.config['UPLOAD_FOLDER'], 'logo-sm.png')
if not os.path.exists(logo_sm_path):
os.system("magick/bin/convert logo.png -resize 10% "
+ os.path.join(app.config['UPLOAD_FOLDER'], 'logo-sm.png'))
return send_from_directory(app.config['UPLOAD_FOLDER'], 'logo-sm.png')Perfect. We can use this to leak the flag.txt file.
A little digging around on github got me this
POC of the exploit.
Crafting the PNG
After cloning the POC script, I simply ran:
python3 CVE-2022-44268.py flag.txtThis creates the output.png file, which I’m going to replace the logo.png with.
Finishing the Exploit
Finally, I uploaded the output.png to the server instance, using Burpsuite to rename
it to ../logo.png.
Now I just had to hit the /logo-sm endpoint. It created the minified version of
the overwritten logo file.
cat-ing the logo-sm.png reveals this piece of suspiciously hex-shaped string:
7363726970744354467b7430305f6d7563685f6d343669635f3139386535616162306238637d0aNoice, just unhex it now.
scriptCTF{t00_much_m46ic_198e5aab0b8c}