🔗 JavaScript Fundamentals

The majority of modern browser exploits rely on a JavaScript bug. When we say "JavaScript bug", we specifically mean a bug that manifests in a JavaScript engine while processing otherwise valid code.

For our purposes, this means that we will be spending a lot of time reading and writing JavaScript. You do not need to be a JavaScript expert to perform vulnerability research on browsers, but it is a good idea to be comfortable with the language.

🔗 JavaScript

JavaScript is a major component of almost every modern website. Most dynamic interactions that happen on the web are driven by JavaScript, and it has become the lingua franca of the web over the years.

The power of JavaScript makes it a hotbed of complexity, while its ubiquity demands high performance. This makes it an especially attractive target:

  • Allows you to run nearly arbitrary code
  • Many complex (read, bug-prone) features
  • Ships with JIT compilers (logic bugs)
  • Optimizations may remove sanity-checks for performance

🔗 Hello World

Let's take a look at the basics of how JavaScript is processed by a web browser. Step one is actually determining which parts of a website are code. There are two major ways that JavaScript can be included from HTML:

JavaScript included inline in HTML:

HTML
<script> console.log("Hello world from JavaScript"); </script>

Loading a remote JavaScript file:

HTML
<script src="/exploit.js"></script>

Either method will result in JavaScript being loaded and ran by the JavaScript engine when your browser parses the webpage.

🔗 The Developer Console

The Developer Console can be very useful when working with browsers. It is essentially a one-stop, fully-integrated debugger for the DOM and JavaScript, allowing you to:

  • Run JavaScript snippets
  • View currently loaded scripts
  • Inspect / modify the DOM

You can open the Developer Console using the F12 key in most browsers.

🔗 Running JS with V8 and JSC

Oftentimes, we will be interested exclusively in the JavaScript engine and will want to examine things without all the extra baggage that comes with running an entire browser.

We can run just the standalone JavaScript engines as follows:

bash
./out/x64.debug/d8 ./path/to/script.js ./WebKitBuild/Debug/bin/jsc ./path/to/script.js

There are some important differences to keep in mind when using standalone versions of the engines:

  • No DOM APIs
  • Missing Web APIs
    • Use print instead of console.log
  • Additional debug methods not normally exposed in-browser

🔗 Readline Wrap

To make the JavaScript engines easier to use, we can use rlwrap:

bash
rlwrap ./out/x64.debug/d8 ./path/to/script.js rlwrap ./WebKitBuild/Debug/bin/jsc ./path/to/script.js

This will allow you to scroll up through old commands/inputs like you normally would in a terminal.

NOTE: You may have issues with rlwrap when debugging with gdb. You can either attach to the js process or use set follow-fork-mode child

🔗 JavaScript Types

Javascript types fall into two categories: Native and Object.

Native types: null, undefined, number, string, symbol

Object types: object

Broadly speaking, native types are essentially a small set of primitives, while nearly everything else in the language falls under the umbrella of each engine's Object class.

Some examples of "special" objects are listed below:

  • Function
  • Array
  • ArrayBuffer
  • RegExp
  • ...

🔗 Weak Typing

Variables in JavaScript are weakly typed, meaning that JavaScript has relatively loose rules about which types of variables can interact with each other. JavaScript will even implicitly perform type conversions over variables if necessary.

For example:

javascript
> '1' / 2 // string to number 0.5 > '1' + 1 // number to string '11'

The set of rules for implicit conversion and general lack of strong-typing can lead to some bizarre outcomes:

javascript
> {}+'' 0 > ''+{} "[object Object]"

For the purpose of this training, you do not need to be intimately familiar with the details of these rules. However, knowing they exist and that they add a lot of unexpected complexity, especially when we talk about Just-In-Time (JIT) compilation, will be helpful.

🔗 Aside: Loose Equals

A particularly non-obvious thing to keep in mind about JavaScript is how the double equals == operator works.

Specifically, == will perform a loose-equals. It will check whether one side "looks" like the other:

JavaScript
> 1 == '1' true > [] == false true > [1] == "1" true > "" == 0 true

Never one to shy away from complexity, JavaScript has a set of rules that govern this loose-equals: JavaScript Equality Table.

🔗 Aside: Triple Equals

Thankfully, JavaScript includes a way to check for strict type equality. Under triple equals ===, only primitives of the same types can be equal. Compare the table below with the one for double equals:

This is much closer to what most languages and people "expect", especially if you are coming from a lower-level or native-code background.

🔗 Variable Declaration

Variables defined inside functions are scoped to that function. The var keyword "hoists" the variable out to the nearest function scope:

JavaScript
function foo() { b = 2; { var b = 3; console.log(b); } console.log(b); } > foo() 3 3

When JavaScript sees a var keyword, it treats that variable as if it was declared at the very top of the function with the special undefined value. This means that in the code above, both b's are the same variable.

The effects of this "hoisting" can be visualized as roughly equivalent to the code below:

JavaScript
function foo() { var b = undefined; // "Hoisted" definition from inner scope b = 2; { b = 3; // Definition moved to the top console.log(b); } console.log(b); }

🔗 Variable Declaration - let/const

The let and const keywords only exist in the scope in which they are declared:

JavaScript
function foo() { let b = 2; { let b = 3; console.log(b); } console.log(b); } > foo() 3 2

As opposed to the code shown previously, the two b's are separate variables in this case.

🔗 Functions

Next, we'll take a closer look at JavaScript functions. Functions are a special kind of object that contain code and can be called. Also note, since they are objects, they can be stored inside JavaScript variables like any other JavaScript type (i.e. functions are first-class).

There are many ways to declare a function in JavaScript:

JavaScript
// Hoisted function declaration function foo(x) { return x*2; } // Assigning function (with default parameter) to variable let bar = function(x, y=2) { return x*2; } // Arrow function syntax let baz = (x) => { return x*2; } // Compact arrow function let foobar = x=>x*2 // Using function constructor let foobaz = new Function('x','return x*2');

All function arguments are optional in JavaScript.

If an argument is omitted, it is set to undefined or its default value.

There are many ways to work with arguments in JavaScript that are non-obvious:

Variable Number of Arguments

JavaScript
function foo(...args) { // Variable arguments console.log(args); } > foo(1,2,3,4) [1,2,3,4]

Accessing args through arguments

JavaScript
function foo() { // Accessing args though `arguments` console.log(arguments) } > foo(1,2,3,4) Arguments(4) [1, 2, 3, 4, callee: (...), Symbol(Symbol.iterator): f]

Using spread and .apply

JavaScript
function bar(a,b,c) { console.log(a,b,c); } > bar(...[1,2,3]) // Spread operator 1 2 3 > bar.apply(null,[1,2,3]) // Applying arguments to a function 1 2 3

Finally, it is worth noting that when a function is defined, its scope is saved in a closure. A closure is a function along with references to all local variables that were in scope when the function was created.

JavaScript
function a(prefix) { let str = prefix + 'bar'; return function() { return str; } } let f = a("foo"); let str = 'baz'; let f2 = a("crow"); > f(); foobar > f2(); crowbar

🔗 Objects

In JavaScript, "Objects" are a generic term/type that are more complicated than native "primitive" types. From a practical perspective, Objects contain key-value pairs called properties that are accessed via the [] or . operators.

JavaScript
> let o = {hello: 'world'} > o['hello'] 'world' > o.hello 'world' > o.foo = 42; > o[100] = 1337; > delete o.foo; // Removes foo

If a key does not exist, the property access returns undefined rather than throwing an error.

🔗 Objects - The Object object

JavaScript comes with a "special" object called Object that has static methods which can perform generic operations on other objects:

JavaScript
let foo = {a:1, b:2}; > Object.getOwnPropertyNames(foo) ['a','b'] > Object.values(foo) [1,2] > Object.entries(foo) [['a',1],['b',2]] > Object.defineProperty(foo, 'c', { value: 3 }); > foo {a:1, b:2, c:3}

You can think of Object as a swiss-army knife for generic operations or queries that you may want to perform over other objects. You can read about all of them here.

🔗 Objects - Object Prototypes

A prototype is a special property that all objects have. At a high level, and in colloquial terms, a prototype can be thought of roughly as a template for a particular "kind" of object. They implement a loose form of inheritance for objects.

However, beware of placing too much weight on this analogy, as the terms "template/type/kind/inheritance" can often mean very specific things in other languages that do not neatly transfer to JavaScript.

When a property is not found on an object, its prototype is checked:

  • Prototype acts as a fallback for not-defined properties
  • Prototypes can have prototypes, creating a prototype chain
  • Multiple objects can share the same prototype

In this way, every property access may end up walking the prototype chain. The engine first checks if the property is defined on the object, then its prototype, and so on.

🔗 Object Prototypes - By Example

An object's prototype can be directly read or written with Object.getPrototypeOf() and Object.setPrototypeOf(). Although technically non-standard / deprecated, accessing or assigning to obj.__proto__ is also widely supported.

Let's walk through an example of manipulating object prototypes:

JavaScript
> let obj = {foo: 1}; > obj.bar // bar is not found in obj, so it searches obj's prototype, still not found undefined

We can fix this by adding bar to the prototype of obj:

JavaScript
> Object.getPrototypeOf(obj) {constructor: f, __defineGetter__: f, __defineSetter__: f, hasOwnProperty: f, ...} > Object.setPrototypeOf(obj, {bar: 2}); // Change the prototype > obj.bar // bar is not found in obj, so it searches obj's prototype, and then finds it 2

And we can continue to extend the prototype chain further:

JavaScript
> Object.setPrototypeOf(Object.getPrototypeOf(obj), {baz:3}) > obj.baz 3

An important caveat to keep in mind when thinking about prototypes and properties is that the prototype is not searched via getOwnPropertyNames and hasOwnProperty, which deals only with properties that are the object's own:

JavaScript
> Object.getOwnPropertyNames(obj) ['foo'] > obj.hasOwnProperty('bar') false

🔗 JavaScript Classes

"Classes" in JavaScript are implemented via object prototypes. Member functions are added to a prototype, and Objects which are instances of that class will use that prototype, thus "inheriting" the member functions.

When a function is invoked as a constructor (with the new operator), the newly created object's prototype is set to the prototype property of the function.

JavaScript
function Cat(name) { this.name = name; } Cat.prototype.pet = function() { console.log(this.name + ": meow"); } let cat_inst = new Cat('fluffy'); cat_inst.pet() // --> fluffy: meow cat_inst.__proto__ === Cat.prototype // true
Noteinfo

The prototype property of a function is not the semantic prototype (i.e. __proto__) of the function, it is a regular property of the function object with name "prototype". It is special in that when constructing an object, it implicitly gets assigned to new_obj.__proto__.

We can accomplish the same thing using the newer and "nicer" Class syntax:

JavaScript
class Cat { constructor(name) { this.name = name; } pet() { console.log(this.name + ": meow"); } } let cat_inst = new Cat('fluffy');

🔗 JavaScript Classes - Inheritance

Prototypes are leveraged for implementing inheritance as well. An instance of a "child" class will have its "parent" class as its prototype:

JavaScript
> class Foo { } > class Bar extends Foo { } > Bar.__proto__ === Foo true

JavaScript will search the prototype chain to find inherited properties.

🔗 Property Descriptors

An interesting and often overlooked feature of JavaScript is the ability to "configure" how certain properties behave. This is done via Property Descriptors. We can see an example of this by using Object.getOwnPropertyDescriptor(...):

JavaScript
> let foo = {a:1, b:2}; > Object.getOwnPropertyDescriptor(foo,'a') {value: 1, writable: true, enumerable: true, configurable: true}

An explanation of each is provided below:

Writable:

  • Controls if this key's value can be changed

Enumerable:

  • Controls if this key shows up in Object.keys or via the in operator

Configurable:

  • Controls if these settings be changed and whether this key can be deleted
  • Controls if you can define Getters or Setters for this key

Modifying or adding property descriptors can be done with Object.defineProperty.

JavaScript
> let foo = {a:1, b:2}; > Object.defineProperty(foo,'a',{ value: 1, writable: false, enumerable: false, configurable: false })

Omitting any of the configurable fields will make them default to false.

🔗 Property Accessors

JavaScript has support for both getter and setter properties. This allows you to define a function that will be executed whenever a particular property is read or written. This is a fairly common feature in many interpreted languages:

JavaScript
let foo = {}; Object.defineProperty(foo, 'magic', { get() { return 1337; }, set(val) { console.log("In setter"); } }) > foo.magic 1337 > foo.magic = 1 In setter undefined

🔗 JavaScript Proxies

Proxies allow you to wrap JavaScript objects and hook various operations.

JavaScript
let target = {}; let p = new Proxy(target, { get: function(obj, prop) { console.log('Getting property ' + prop); return 1337; }, set: function(obj, prop, value) { console.log('Setting property ' + prop + ' to '+value); } });

In this example, we create a proxy and have it conceptually behave just like the getter and setter from the previous section.

However, Proxies allow you to hook objects with much higher fidelity. You can hook all of the following using proxies:

  • has
  • deleteProperty
  • construct
  • apply
  • ownKeys
  • getOwnPropertyDescriptor
  • defineProperty
  • getPrototypeOf
  • setPrototypeOf
  • isExtensible
  • preventExtensions

If a proxy does not define a hook, the operation will proceed through to the original target as if the proxy was not there.

🔗 JavaScript Arrays

Arrays are special objects with a length property. Typically, they act as generic storage containers for other variables, and they are used similarly as in other languages:

JavaScript
// Almost same as {0:'a',1:'b',2:'c',3:'d'} > let a = ['a','b','c','d']; // Getting length property retrieves length > a.length 4 // Setting index expands array > a[100] = 1 > a.length 101 // Setting length shrinks or expands array > a.length = 2 > a ['a','b'] // Arrays have special methods from their prototype > a.__proto__ === Array.prototype true

🔗 JavaScript Typed Arrays

JavaScript has separate support for Arrays that are meant to hold binary data. These are known as Typed Arrays.

Conceptually, Typed Arrays behave somewhat like arrays in strongly typed, lower-level languages:

JavaScript
// We can make typed arrays with a certain data size > let uint8_buff = new Uint8Array(0x100); > let uint32_buff = new Uint32Array(0x100); > let float64_buff= new Float64Array(0x100); // Elements in this array will be truncated to that size > uint8_buff[0] = 0x4142; > uint8_buff[0] 0x42 // ArrayBuffers hold generic base data let buff = new ArrayBuffer(1000); // We can make a view that accesses the data with different types let ui8_view = new Uint8Array(buff); let f64_view = new Float64Array(buff);

🔗 JavaScript Numbers

Numbers in JavaScript are, by the specification, 32-bit signed integers:

JavaScript
>> 0xffffffff|0 -1 >> 1337 << 32 1337

This is an important and non-obvious point to keep in mind. Numbers that don't "fit" into a 32-bit signed integer are automatically converted to a double-precision floating point value.

We can find the largest integer that a double-precision float can represent exactly:

JavaScript
> Number.MAX_SAFE_INTEGER 9007199254740991

Of course, as soon as this threshold is crossed, we begin losing precision due to the nature of floats:

JavaScript
// Beyond this point precision is lost to floating points > Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 true

As a consequence of this, it can be tricky to represent arbitrary 64-bit data:

JavaScript
> 0x4142434445464748.toString(16) 4142434445464800

🔗 JavaScript Numbers - Double Bit Patterns

When writing an exploit, we will often abuse doubles to pull off a type confusion. This usually means smuggling a 64-bit pointer into a float, but more generally it is about creating a double value with a specific bit pattern.

Almost all 64-bit numbers represent a unique floating point number. Meaning, almost every 64-bit-pattern maps to some number that is represented using the float specification.

Importantly, there are certain values we cannot represent due to certain high-bits being set. Those bits force the float to be treated as NaN (Not-A-Number), though we will expand on this when it becomes relevant later on.

For now, you can get an idea of how doubles work using float.exposed.

🔗 JavaScript Numbers - Big Ints

As you may imagine, the lack of arbitrary precision integers in JavaScript is the source of much frustration. This feature was added to JavaScript via BigInt().

You can use them as follows:

JavaScript
// Use either BigInt() or a constant followed by n > BigInt(0x4142434445464748) === 0x4142434445464748n true > 0x4142434445464748n.toString(16) 4142434445464748 > 1n + 1n // You can only do math on bigints 2n > Number(1337n) // Convert back to a number 1337

Although very useful for general programming tasks, we will not focus much on them. BigInts have more member data than doubles (which are essentially a native type), which makes them much trickier to use for type confusions.