Attacking Microsoft Edge to identify users by leaking URLs from Fetch requests

TL;DR

I found that Microsoft Edge exposes the URL of any JavaScript Fetch response, in contradiction to the specification. This is a problem because it is possible to identify users by crafting a Fetch request in a webpage that will redirect to a URL containing the visitor’s username (e.g. requesting https://facebook.com/me will result in https://facebook.com/username). This may obsolete identification by IP or browser fingerprinting.

Introduction

Although I am generally less interested in web development issues, I’ve recently had the time to play with some of the relatively new web APIs, such as WebSockets and Service Workers. I tried testing the boundaries of Service Workers, hoping to find anything that compromises the users’ privacy. I didn’t find anything with Google Chrome, but the first few minutes playing with Microsoft Edge I found the following information exposure in the Fetch API.

I would generally not bother writing about this and just let Microsoft resolve it, but their security team refused to acknowledge it as an issue, so I hope that publishing it may convince them otherwise.

Please make yourself familiar with Fetch. In short, it is the cleaner, modern replacement for XMLHttpRequest (XHR).

The Issue

By default, requests made with fetch should respect CORS and deny access to inappropriate resources. However, it is possible to make a request with “no-cors” mode, enabling fetch to bypass CORS. The response of such a request should be “opaque”, meaning it’s content should be empty (so that an attacker could not gain access to cross-origin requests made with the user’s session). The specification states the following:

An opaque filtered response is a filtered response whose type is “opaque”, url list is the empty list, status is 0, status message is the empty byte sequence, header list is empty, body is null, and trailer is empty.

The vulnerability is that in Microsoft Edge the url property of an opaque response is not an empty string, but it is the full URL of the response returned after following redirects.

This may not sound like an issue at first since no confidential information should be stored in the URL. That’s correct. However, many websites use redirection to redirect the user to a personal page. For instance, a website redirecting https://example.com/profile to https://example.com/my_username.

Some real life examples used in the PoC are https://facebook.com/me, https://youtube.com/profile and https://docs.com/me. Redirection to the user page is just an example and there are other possibilities to exploit this issue, left to the reader as an exercise.

Proof of Concept

Test your browser or see the GitHub project (run poc_facebook.py for the Facebook example)

I originally tried to find a Microsoft service that the issue applies to, so the first PoC targets https://docs.com. I later modified it a little to target Facebook. The simpler version only proves the URL leak happens with Docs, Facebook and Youtube. Anyhow, the effect is that without any user interaction or consent the user’s identity is leaked to the attacker’s website.

I’ve tested the PoC on Microsoft Edge 40.15063.0.0 (slow ring) and on Microsoft Edge 38.14393.0.0 (current release).

Microsoft’s response

The response:

Hello Ariel,

We have determined that this does not met the bar for security servicing through a security bulletin as the only information disclosed is the URL, which should not contain sensitive data. The product team has opened a tracking item to address this in a future version update.

We appreciate your report. If you have any additional concerns or questions, please let me know.

Cheers,

Update (24.4.2017): I’ve reached MSRC once again after this post became popular on Reddit and social media. Their response clarified that they do treat this as a security issue and assured a patch is to come soon:

Hi Ariel,

We have always considered this a security issue, it just did not meet our bar for security servicing through a bulletin and would be addressed with a next version fix. The product team is considering moving up their timeline to the June Patch Tuesday release for this. As we get closer to this date I will be able to provide you more details regarding the fix and how it will be shipping. Currently because they are targeting a Patch Tuesday release, we will be assigning a CVE as well as providing acknowledgement for the report since you followed CVD, which we do greatly appreciate.

Cheers,

Bug hunting GDK-PixBuf

GDK-PixBuf is an image loading library, mainly used by GTK+. It was originally a part of GDK but it was split up to it’s own repository. I’ve found multiple vulnerabilities in it that I had reserved CVE IDs for given it’s widespread use.

If you are reading this merely for the for the explanations of the bugs feel free to skip the following introduction (or go read the bug tickets).

A little on fuzzing

I recently had some time to read about a great deal of security vulnerabilities and other bugs found with fuzzing, and it occurred to me to try and fuzz some libraries and applications I use regularly on my Linux machine. After a while I got to gdk-pixbuf. It is a great fuzzing target, because it is both a widely used library and it deals with various types of inputs.

My fuzzer of choice for this was american fuzzy lop (afl). It is very straight forward, it is well documented, and among other benefits it should be able to generate different file formats by itself, to reach full code coverage (for example). There’s more to it, but afl is not the topic of this post.

To start fuzzing, I had to either make a binary that uses gdk-pixbuf or pick something that uses it. I used gdk-pixbuf-thumbnailer which is a part of the gdk-pixbuf repository (originally a part of gnome-desktop that had been moved to an external thumbnailer binary). It is called by gnome-desktop to generate thumbnails for images.

Before this commit gnome-desktop fell back to thumbnail the file only if no external thumbnailer was found for the file’s format (in other words, gdk-pixbuf-thumbnailer exists because the developers wanted to move the internal gdk-pixbuf thumbnailing process to an external binary). Vulnerabilities had been found in one of these external thumbnailers, evince-thumbnailer.1

To really get the best out of afl, the source code of the target must be compiled with an afl wrapped compiler (afl-gcc or afl-clang). These compilers inject afl instrumentation during compilation. Even better is afl-clang-fast which does “true compiler-level instrumentation, instead of the more crude assembly-level rewriting approach taken by afl-gcc and afl-clang”.2

The best way to compile Gnome projects from trunk is with JHbuild. It’s great because it allows compilation of the most recent Gnome internals without breaking anything (everything it builds go to the configured JHBuild path). I added os.environ['CC'] = '/usr/bin/afl-clang-fast' and os.environ['CXX'] = '/usr/bin/afl-clang-fast++' to my ~/.config/jhbuildrc and compiled it with jhbuild build gdk-pixbuf. Now I had a gdk-pixbuf-thumbnailer binary that is ready to be fuzzed.

There are some other steps I did before actually starting afl with the binary, but as this is getting long already, I won’t elaborate. If you are interested in using optimizing and afl though, read this post about afl.

First bug (CVE-2017-6311)

A crash! and another! Not a second passed before afl found 4 unique crashes. A quick investigation with gdb revealed there was a bug in the gnome-thumbnailer-skeleton.c file in the main function. It would try and print error->message when error was not set (it was NULL). This would translate to printing whatever is in address 0x8.

This should generally lead to a crash and nothing beyond that, at least on a user space program. But it’s definitely a problem, so I filed a bug and it was eventually assigned a CVE ID by MITRE among the other bugs.

An optimization driven bug (CVE-2017-6312)

I let afl run for a while until it found a new unique crash. It was a segmentation fault. I examined it with gdb:

Program received signal SIGSEGV, Segmentation fault.
DecodeHeader (Data=0x806caf8 "", Bytes=<optimized out>, State=<optimized out>, error=<optimized out>) at /home/tester/jhbuild/checkout/gdk-pixbuf/gdk-pixbuf/io-ico.c:362
362			if ((BIH[16] != 0) || (BIH[17] != 0) || (BIH[18] != 0)

This time the error is on io-ico.c. In the gdk-pixbuf folder there are different io-*.c files, each of these is a loader for a different file format. That is the decoder for Windows icon file format. I didn’t have to investigate much to find out what the problem was:

BIH = Data+entry->DIBoffset; 

/* A compressed icon, try the next one */
if ((BIH[16] != 0) || (BIH[17] != 0) || (BIH[18] != 0)
	|| (BIH[19] != 0)) {

I thought that’s about it, and proceeded to try and figure out if this bug could be used for anything besides crashing the binary. But something then bothered me - when I compiled the binary for debugging (with -O0 flag), I was suddenly no longer seeing any segmentation faults.

I was sure I got it wrong, so I tried a few more times, compiled it back with optimization flags (the default is -O2) and it crashed. Otherwise I would get this error: ** (gdk-pixbuf-thumbnailer:12409): WARNING **: Could not thumbnail '/home/tester/crashes/test.ico': Invalid header in icon (header size). So somehow compiler optimizations allowed the bug to happen!

I began isolating individual compilation flags (that are added by -O2) to identify exactly what was the cause of the trouble. After a short while, I got it right. -O1 -fstrict-overflow -ftree-vrp were the flags that, when combined, let the crash happen.3 If you read the gcc documentation on -fstrict-overflow, it explains this flag is used to allow the compiler to assume integer overflow never happens. ftree-vrp “allows the optimizers to remove unnecessary range checks like array bound checks and null pointer checks.” (from the gcc manual).

Let’s see what caused the error print on the good runs:

State->HeaderSize = entry->DIBoffset + INFOHEADER_SIZE;

if (State->HeaderSize < 0) {
	g_set_error (error,
	             GDK_PIXBUF_ERROR,
	             GDK_PIXBUF_ERROR_CORRUPT_IMAGE,
	             _("Invalid header in icon (%s)"), "header size");
	return;
}

This now partly makes sense. If the compiler assumes that integer overflow never happens, we get undefined behavior by definition when an overflow does happen. This is possible on the first operation that sets State->HeaderSize, since INFOHEADER_SIZE is defined to 40 and we have full control of entry->DIBoffset. From a quick disassembly of the binary, it seems the overflow check is just left out.

I tested this on Arch Linux and later on Ubuntu 16.04.1 and it crashed eog and nautilus. Luckily, the code doesn’t write anything to BIH but only reads from it. So to conclude this bug, it may lead to an out-of-bounds read to an address which we control, or otherwise lead to undefined behavior.

Link to bug report

Finding more bugs (CVE-2017-6313)

With afl running in the background, I started roughly reviewing the code manually. After some playing around with the loaders I found a bug in io-icns.c, the loader for Macintosh icons. There is a possible integer overflow in the size variable that is used later in a call to gdk_pixbuf_loader_write. This function is the general loading function, it finds the correct loader and calls it to decode the image in the buffer given to it. Let’s take a look at it’s signature:

/**
 * gdk_pixbuf_loader_write:
 * @loader: A pixbuf loader.
 * @buf: (array length=count): Pointer to image data.
 * @count: Length of the @buf buffer in bytes.
 * @error: return location for errors
 ...
 **/
gboolean
gdk_pixbuf_loader_write (GdkPixbufLoader *loader,
			 const guchar    *buf,
			 gsize            count,
                         GError         **error)

The underflow is in the load_resources function which is called to decode the icns. There are multiple possible underflows in the *mlen = and *plen = lines. The first case:

if (memcmp (header->id, "ic08", 4) == 0	/* 256x256 icon */
	|| memcmp (header->id, "ic09", 4) == 0)	/* 512x512 icon */
{
	*picture = (gpointer) (current + sizeof (IcnsBlockHeader));
	*plen = blocklen - sizeof (IcnsBlockHeader);
}

sizeof(IcnsBlockHeader) will always be 8, so if we have blocklen < 8, plen is underflown. Later gdk_pixbuf_loader_write is called with all the data after the icns header, and the plen/mlen as read to count.4 So it is possible to call any loader with a huge (or negative, depending on type) count.

A malicious file would start like this:

0000h: 69 63 6E 73 00 00 00 10 69 63 30 38 00 00 00 07  icns....ic08....

blocklen here becomes 7, so plen will end up with the value of -1. If used as an unsigned variable (assuming 4 bytes) it’s value will be 232-1.

From a quick trial and error of this flaw with different image types I’ve found two out out-of-bounds reads. Both io-bmp.c and io-ico.c (which is actually based on io-bmp.c), when given a big count, continue to read from buf beyond it’s real size, resulting in a segmentation fault.

Infinite loop (CVE-2017-6314)

Another interesting behavior is when trying the icns bug with a tiff image. It would hang, indefinitely. After digging into io-tiff.c, I found the culprit. The make_available_at_least function is called to allocate a buffer as large as the size we give it. Let’s take a look on few lines this function:

...
if (need_alloc > context->allocated) {
        guint new_size = 1;
        while (new_size < need_alloc)
                new_size *= 2;
...

The problem is that when new_size is large enough, multiplying it by 2 will overflow itself. Specifically, since it starts with 1, when it reaches 231, the multiplication should result in 232 but that’s too big for a 32 bit integer, so the result of the operation is 0.

Since this can be reproduced with a regular huge tiff file (without the icns vulnerability), I filed a specific bug for this issue.

Other possibillites

By stacking two icns headers in a file, I was able to reach another out-of-bounds read. With the right filesize on the second header, I also caused another infinite loop (thanks to the out-of-bounds read returning zeros). With the same setup I was also able to cause the program to try allocate a buffer with the size I give it.

The latter is also possible when a gif image or a tga image is placed after the icns header. Otherwise, if the image data is less than 4096, it would lead to another out-of-bounds read of the data.

Link to bug report

What is affected?

There are many Linux user space applications that rely on gdk-pixbuf to load and display images. This also affects desktop managers themselves (the obvious is Gnome) and some file managers. To see the full list of programs affected run apt-cache rdepends libgdk-pixbuf2.0-0 libgdk-pixbuf2.0-dev (on apt based distributions). I originally intended adding a demonstration of using one of these bugs maliciously, but I decided to leave that out for now.

I also found out these bugs can crash both Firefox and Chromium by trying to load a bad file from their file manager.

Until next time

I thought about writing another post on libwmf, but I eventually reconsidered it. libwmf is another widely used library in which I’ve found some vulnerabilities. I reccomend disabling it if it is enabled in your distribution.

  1. CVE-2010-2640, CVE-2010-2641, CVE-2010-2642, CVE-2010-2643 

  2. afl-2.39b/llvm_mode/README.llvm 

  3. Using -O1 -fstrict-overflow or -O1 -ftree-vrp wouldn’t allow the crash. 

  4. The developer’s intention was to load a JPEG 2000 image that usually follows the header for icons of size 256. However, no check is made that a real JPEG 2000 follows so we can abuse this call.