Anyways, after downloading and installing math.js locally, I started poking around. The first thing I noticed was that everything seemed much more secure. Trying to retrieve a Function object is not longer possible.

> math.eval('cos.constructor')
Error: Access to "Function" is disabled
at getSafeProperty (/home/samczsun/test/node_modules/mathjs/lib/utils/customs.js:16:11)
at Object.eval (eval at <anonymous> (/home/samczsun/test/node_modules/mathjs/lib/expression/node/Node.js:71:19), <anonymous>:3:390)
at string (/home/samczsun/test/node_modules/mathjs/lib/expression/function/eval.js:43:36)
at Object.compile (eval at _typed (/home/samczsun/test/node_modules/typed-function/typed-function.js:1115:22), <anonymous>:22:14)
at repl:1:6
at sigintHandlersWrap (vm.js:22:35)
at sigintHandlersWrap (vm.js:73:12)
at ContextifyScript.Script.runInThisContext (vm.js:21:12)
at REPLServer.defaultEval (repl.js:340:29)
at bound (domain.js:280:14)

Clearly a different approach would be needed.

I learned from the readme that math.js eventually delegates all the math to eval'd JavaScript, which means that at some point it must be generating the code dynamically. After some digging, it turned out that math.js was using new Function(params, code). To make my life a bit easier I hackily intercepted the Function constructor to print out the generated code. In hindsight, I could have just modified the mathjs source.

Looks pretty hopeless. I might be able to trick some handwritten security but I didn't want to try and find a type confusion bug in JavaScript itself.

Instead, I turned my focus on the generated code itself. What if there was a way to inject some malicious code a la XSS?

I knew that I controlled the strings themselves, but not any of the wrapper code around it. I also knew that the code was all on one line, which meant that I would be able to wipe out any code after my payload with a simple //. The question is really "how do I trick the lexer".

It turns out that the answer was very simple. Why bother tricking the lexer when you could just modify the results of the lexer!

First off, I noticed that the implementation of eval simply delegates the call to parse(code).compile().eval(). However, while the internal parse function is not visible, a separate parse function is exported for public use. This parse returns a node tree - just what we want.

While I was poking around, math.js still had a hardcoded filter which prohibited accessing the constructor property. In my infinite wisdom I parsed cos.constructor instead of cos, which caused my payload to fail on the math.js online API. This then caused me to spend a lot more time reworking my payload. If you're interested in that process, I included a bonus writeup below.

Awesome! At this point, I've basically progressed this payload to the point where it'd be trivial to just repeat the steps from CapacitorSet and denysvitali's blog post. As such, that is left as an exercise to the reader. Instead, I sent off an email and called it a day.

Timeline

Still here? If you have no idea what this is, not all was as nice and easy as I made it seem. In reality I goofed up and my original parse payload didn't work due to a silly mistake. Here's what actually happened:

After being thoroughly disappointed that my payload didn't work on the web API, I naturally picked the harder solution of redeveloping the entire payload instead of removing a single word.

I knew I still wanted to go with the "inject malicious code into the syntax tree" approach, but there was a little roadblock:

function SymbolNode(name) {
if (!(this instanceof SymbolNode)) {
throw new SyntaxError('Constructor must be called with the new operator');
}
// validate input
if (typeof name !== 'string') throw new TypeError('String expected for parameter "name"');
this.name = name;
}

Uh oh. math.js's eval doesn't support new. Fortunately, we can make use of JavaScript's prototypes.

We start off by constructing a regular object.

s={};

Then, we can update the object's prototype to match the SymbolNode's prototype.