This weekend, I participated in RedpwnCTF with my team Starrust Crusaders under the alias "The Static Lifetime Society", coming in second place overall. Since this was my first time doing a Chromium SBX challenge, I thought it would be a good idea to make a writeup. I'm still relatively unfamiliar with Mojo concepts and sbx escape, so feel free to point out any mistakes I make.
Here are some relevant resources I used to help me understand Chromium's architecture and sandbox escape techniques. I would recommend giving them a read before continuing.
We were given a Chromium binary, mojojs bindings, and a source patch. The only difference between the two parts is that part one is run with the CTF_CHALLENGE_EASY_MODE environment variable set. Chromium is also run with MojoJS bindings enabled. Usually in fullchain exploits, one would have to compromise the renderer first, overwrite the blink::RuntimeEnabledFeatures::is_mojo_js_enabled_ variable, refresh the page, and then attempt to escape the sandbox. Since we already have bindings enabled, we won't have to worry about any of that. Here's the provided patch:
The Wreck struct holds a size, a length_to_use, an optional BigBuffer, and a DesertType enum with variants DESOLATE and EMPTY. Sand is an array of wrecks. The most important struct is Ozymandias, which has the methods Visage and Despair, a pointer reserved for a mmap page and an UnguessableToken. UnguessableTokens are cryptographically secure 128 bit random values. Note the author commented that the struct is 0x100 bytes, which can be verified as well when it hits in the new operator in that function (find the mangled name, and attach to the main privileged browser process to break).
As for the html website to send to the browser bot, I used the following (which was based off of NotDeGhost's post):
Here was the result from running the exploit on remote from my teammate's VPS (thanks Strellic!):
Now, the env variable is disabled. So what could possibly allow us to leak UnguessableTokens then? I have to admit that this step took an embarrassingly long time.
I first spent several hours reading up on this post about common Chromium IPC vulns and tried to compare them to the diff. Nothing was found this way.
Another idea teammates hypothesized was that maybe there is a TOCTOU race condition between when it checks for size and length_to_use and when length_to_use is used. However, data is serialized when passed to privileged browser process, so us trying to race in renderer is useless and will not apply changes there (and the window is too small since we need to abuse sizes of approximately 0x100).
Lastly, what really caught my attention is that if you have it not hit any of the cases in the switch statement, you will still get an uninitialized read. To do that, you need a new enum value, and unfortunately, Mojo validates all enums (unless the keyword extensible is used) along with several other validation checks. I thought this was an interesting scenario as we can change how we send things from the mojojs bindings and wanted to see if validation can be bypassed somehow. As I was messing around with wreck types, I noticed that I suddenly achieved a leak when I set my BigBuffer tag to 2, which stands for invalid storage. Debugging the codeflow, I noticed that the contents of the DesertType enum became 0 there somehow (if you set it to zero normally from renderer before serializing and sending, a validation error will occur and your renderer process will be killed). I wasn't too sure why, but according to NotDeGhost after my solve, it is because this enum causes deserialization to fail, so the rest of the struct will not be populated (hence leaving DesertType to 0). The NOTREACHED() statement in the diff is not compiled into release builds, allowing for us to trigger this bug and get uninitialized read when it works with wreck structs.
This time, however, we can't just spray some allocations and have an OOB leak data outside the chunk. We will need to free some OzymandiasImpl objects, which can be done pretty easily with .ptr.reset() as it is an interface implementation. Here is the final exploit:
Here is the result from remote:
Overall, I thought these were amazing challenges and finally pushed me to mess around a bit with Chromium sandbox escapes. Though introductory challenges into this complex topic, many of the concepts and basic techniques can probably be re-applied on more difficult sandbox escape challenges.
Here is the author's writeup! Make sure to check it out.