Exploit Archeology - Exploiting an old unknown Server Side Browser

I was recently hacking on a Bug Bounty target and identified an interesting API endpoint which would render user supplied HTML, and execute any included JavaScript. Exploiting Server Side Browser bugs has been a focus of mine for the past couple of years, so I set out to exploit this newly identified feature. This blog post details my journey into researching and exploiting what turned out to be a decade old Server Side Browser.

Recon

The first step was to identify the Browser engine and version in use, this is usually a pretty simple task, of checking the User-Agent header of requests for external resources. So I sent an HTML payload into the target service with an img tag loading an image from a server I control (Side note: I use https://github.com/ajxchapman/reserv for all my server request bug hunting). This revealed the User-Agent of:

Mozilla/5.0 (Unknown; Linux x86_64) AppleWebKit/538.1 (KHTML, like Gecko) Safari/538.1

… which is really not very helpful. You’d usually expect to see a Chrome/124.0.0.0, Firefox/125.0, or similar in there at least somewhere, which can disclose the Browser engine and version. No such luck here.

However, since I had JavaScript execution, I could concretely identify the Engine through browser specific APIs, e.g Error.captureStackTrace for Chrome, document.preferredStyleSheetSet for Firefox, and webkitConvertPointFromPageToNode for WebKit (Safari). In this case it appeared to be a WebKit based Engine.

From this, my first (and as it turned out, first incorrect) assumption was it was probably PhantomJS. I’d previously written an exploit for CVE-2014-1303, so packaged that up and sent it into the HTML service. No result… although I tried to two or three (or seven) more times just to be safe.

Begrudgingly accepting that it wasn’t PhantomJS, I set out to work out what approximate version of WebKit it was running. To do this, the Browser Comparison feature of caniuse.com came in really handy. Selecting several Safari versions, I could easily compare the feature support for each version:

Can I Use Safari feature comparison

There are some discrepancies with the data from caniuse.com and MDN, using both of these sources a basic script could be created to determine the rough version of WebKit in use:

var version;
version = !version && typeof fetch == "function" ? "Safari WebKit 10.1" : version;
version = !version && typeof String.prototype.includes == "function" ? "Safari WebKit 9" : version;
version = !version && typeof document.currentScript == "object" ? "Safari WebKit 8" : version;
version = !version && typeof window.requestAnimationFrame == "function" ? "Safari WebKit 7" : version;

console.log(version);

This revealed that the service was using WebKit that corresponds to Safari ~8, with an absolute date circa 2014 to 2015. Yes, nearly a decade old! Time to get my hands dirty and dust off my vulnerability excavation tools.

Research

Now that an approximate WebKit version and code development timeframe was known, I could go digging for old vulnerability reports, PoCs and exploits. Searches for WebKit vulnerabilities around that time came up with a lot of results for Sony PlayStation Jailbreaks. PS Vita had been released in 2011, and PS4 in 2013, and a common entrypoint to Jailbreaking these devices was WebKit. This was great news for me, as there were a fair number of PS Jailbreaks exploiting WebKit versions from around the time I was looking.

Hours of reading various Jailbreaking community sites, forums, wikis and GitHub repositories revealed several viable vulnerability candidates, from authors with names such as qwertyoruiop, Fire30 and RKX1209. Unfortunately for me, most of these main characters in the ~2015 PS Jailbreak scene were fond of writing illegible exploits, using minimal (and sometimes downright incomprehensible) comments and undefined static offsets all over the place. Some of the code was truely from another era.

I eventually unearthed one viable looking exploit with a clear writeup by a hacker known as xyz. The exploit targeted a Use After Free (UaF) vulnerability in the WebKit JSArray::sortCompactedVector function. Further searching did not identify a CVE related to this issue though, so I approached with some trepidation.

The JSArray::sortCompactedVector exploit writeup from xyz included a basic PoC, which could be used to confirm whether the target service was vulnerable or not:

var almost_oversize = 0x3000;
var foo = Array.prototype.constructor.apply(null, new Array(almost_oversize));
var o = {};
o.toString = function () { foo.push(12345); return ""; }
foo[0] = 1;
foo[1] = 0;
foo[2] = o;
foo.sort();

So I batched this up in a <script> tag and sent it off to the target service. I got no response. This could have been due to the service crashing, or it could have been due to me not including any external logging :facepalm: With logging in place, I was able to confirm the target would crash when processing this script, fantastic!

It was at this point I realised that getting a reliable exploit using only a remote service was 1) going to increase the development and debugging time greatly, and 2) cause the remote service to crash a lot. I needed a local development environment to PoC this out on, before testing against live again.

Fortunately for me, my old friend PhantomJS was also vulnerable to this JSArray::sortCompactedVector UaF, so I could use that as a stand-in target for the exploit development phase.

Development

My first task was to actually get PhantomJS compiled with symbols (to make my inevitable exploit debugging a little less sanity destroying). The last public release of PhantomJS was from 2016(!), so pulling the code and starting with libraries from around that time seemed like a good start. After quite some attempts, I was able to compile PhantomJS with the following:

docker run -ti --rm -v`pwd`:/working ubuntu:14.04 /bin/bash
apt-get update
apt-get install build-essential g++ flex bison gperf ruby perl \
 libsqlite3-dev libfontconfig1-dev libicu-dev libfreetype6 libssl-dev \
 libpng-dev libjpeg-dev python libx11-dev libxext-dev git


git clone https://github.com/ariya/phantomjs.git
cd phantomjs
git checkout 2.1.1
git submodule init
git submodule update
python build.py --qmake-args="QMAKE_CFLAGS=-g" --qmake-args="QMAKE_CXXFLAGS=-g"

Fortunately, the Ubuntu package repository for Ubuntu 14.04 is still live, making this a much less painful process than it otherwise could have been. With that done, it was time to get developing the PoC.

Following the exploit from xyz, all is going well until I got to:

Interestingly, this successfully corrupts a JSArray object on the Vita but crashes on Linux hitting a guard page. But this doesn’t matter because we’re not exploiting Linux.

But I am exploiting on Linux xyz! What do I do now??!? On PS Vita, the vulnerability can be used to scan contiguous memory pages for a target to corrupt, to gain the basic read, write and addrOf primitives. The issue on Linux is that guard pages, memory pages without rw permissions, are placed in between rw pages, breaking up the contiguous memory region. If you attempt to read or write to a guard page a SIGSEGV will be raised, killing the process, which is less than ideal.

For those not intimately familiar with browser exploitation, these primitives are the basic building blocks of most exploits. The read primitive allows reading any memory address, the write primitive allows writing an arbitrary value to any memory address, and the addrOf primitive discloses the memory address of a given JavaScript object. From these three primitives, arbitrary shellcode execution can eventually be built.

At this point I was left with a corrupted ArrayWithDouble (an array with all elements being of type double) with a length of 0x80000000, allowing out of bound (oob) read and write to memory after the corrupted array. By placing an object array in memory after the corrupted array, the addrOf primitive could easily be created, leaving the read and write primitives. To create the read and write primitives, the backing address pointer of a Float64Array can be corrupted, making the arbitrary memory addresses referenceable. Unfortunately, to corrupt this pointer requires a pointer dereference, so cannot simply be done using the corrupted oob array read / write. I needed an arbitrary memory write to create my arbitrary memory write it seemed :facepalm:

Scanning through the oob contents of the corrupted array, I noticed a pointer to the start of the current memory region. Using this, it was possible to reliably calculate the address of the corrupted array. At this point an idea formed. If I could get a Float64Array in memory after the oob array (and learn it’s address using the addrOf primitive), and it’s TypedArray pointer (the structure which hold the backing store pointer) was also after the oob array, I could use relative reads and writes from the oob array to corrupt the backing store, without hitting those pesky guard pages. Some revisions and cursing later, this technique gave me the final read and write primitives.

Was this the most efficient method for exploiting this issue on Linux? I have no idea! As you may have noticed by now, I am by no means a expert exploiting ancient WebKit issues. If you know of an easier method I could have used drop me the details on Twitter!

Exploitation

With the basic primitives in hand, the final step was to use them to get arbitrary shellcode execution. In the PlayStation Jailbreaks I was learning (*cough* copying *cough*) from, Return Oriented Programing (ROP) was always used in the final stage to execute the desired shellcode. In this case, I didn’t have access to the target executable, only my PhantomJS based development environment, which wouldn’t do for ROP chain development. So I came up with a new great idea, I’d scan the executable memory dynamically looking for the ROP gadgets I needed to map rwx memory, copy in shellcode and jump into it… how hard could it be?

A couple of notes at this point 1) It would be hard! 2) If I had stopped to think, rather than blindly follow what others before me did I would have saved myself a world of pain.

After a long time of playing with ROP, I got a feel for what gadgets I would require to achieve shellcode execution, so then I just had to write ROP gadget discovery and ROP chain compilation functions in JavaScript. With basic gadget discovery implemented I ran it against the target to see if it would even work, which it didn’t! The first couple of dozen memory reads worked as expected, but then odd results were being returned, which did not appear to be consistent with what I was expecting.

Hours of Debugging later

I realised, due to repeated calls, the read primitive function was being Just In Time (JIT) compiled. During JIT compilation, the corrupted array length check was re-added, preventing overwrite of the Float64Array backing address. I had very much mistakenly assumed that JIT was either not yet implemented in this WebKit version, or had been disabled like in PhantomJS. Editors Note: JIT is NOT disabled in PhantomJS, another assumption that has cost me significant time writing exploits :facepalm:.

JIT is a gift from browser developers to exploit authors. Not only is it hugely complex, introducing many vulnerabilities in browser engines, it also necessitates memory with rwx protections (or at least rw- then subsequently updated to r-x).

In this version of WebKit, JIT pages were left with rwx memory protections. So despite all my ROP research and development, the actual method of gaining shellcode execution I ended up with was to simply force a function to be JIT compiled, then overwrite the resulting function instructions in memory, then call the function. Much easier!

Conclusion

After painstakingly producing this reliable Remote Code Execution PoC and confirming execution on the target, I submitted the Bug Bounty report very much hoping for, if not actually expecting, a big payout… can you guess where this is going yet? The Bug Bounty program promptly responded to my report, praising the amount of effort that had clearly gone into this issue and informing me the vulnerability actually exists in a 3rd party service. Oh. On to reporting the issue to the 3rd party, who doesn’t have a Bug Bounty program… and who never responded to any of my multiple emails attempting to disclose the issue to them.

Oh well… on to the next one.

References