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 ofconsole.log
- Use
- Additional debug methods not normally exposed in-browser
Readline Wrap
To make the JavaScript engines easier to use, we can use rlwrap
:
bashrlwrap ./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:
JavaScriptfunction 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:
JavaScriptfunction 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:
JavaScriptfunction 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
JavaScriptfunction foo(...args) { // Variable arguments
console.log(args);
}
> foo(1,2,3,4)
[1,2,3,4]
Accessing args through arguments
JavaScriptfunction 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
JavaScriptfunction 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.
JavaScriptfunction 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:
JavaScriptlet 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.
JavaScriptfunction 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
NoteThe
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 tonew_obj.__proto__
.
We can accomplish the same thing using the newer and "nicer" Class syntax:
JavaScriptclass 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 thein
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:
JavaScriptlet 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.
JavaScriptlet 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.