Party Cat0%

26/01/26

HowJSON-JavaScriptDiscrepancyLeadstoRCE(Lodash)

Official Writeup: gap | 0xL4ugh v5

Thanks for sharing!

بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

How JSON-JavaScript Discrepancy Leads to RCE (Lodash)
"Once you see it, you can't unsee it."
Hello!! "Gap" challenge writeup is finally here, this challenge wasn't about finding a complex gadget chain in the application logic. It was about exploiting a fundamental misalignment between how JSON defines data and how JavaScript defines code. Believe me, you are gonna enjoy this one!
We are going to trick lodash.template into treating a single JSON object key as multiple function arguments, allowing us to smuggle in an RCE payload via ES6 default parameters. Let's break it down step-by-step.

1. The Function() Constructor

To understand this exploit, you have to understand exactly how new Function() works in JavaScript. It is the core mechanism Lodash uses to compile strings into executable templates.
Most developers see this:
But the first argument(s) are just strings. If you pass an array, JavaScript automatically joins it with commas.
The JavaScript engine parses that string ("a,b") as a formal parameter list. It doesn't care that a and b came from an array or a single string. It just sees the comma as a separator.
Where is the dnager? If we can control that string, we can inject commas. If we can inject commas, we can define new arguments that the developer never intended to exist.. 😃

2. The Bug: Trusting Object.keys()

In lodash.template, the library allows you to pass an imports object. This is meant for helper functions (like formatDate).
Here is the actual vulnerable code from lodash.js:
Lodash merges the user options with settings, then extracts the keys and values. Crucially, it blindly trusts importsKeys.
A few lines later, it constructs the template function:
The issue here is that importsKeys is passed directly as the first argument to Function. It assumes these keys are just variable names. It performs no validation.

3. Comma Injection

This is where JSON and JS collide.
In JSON, this is a single key:
When Lodash processes this:
  1. Keys: ["context, injected"] (Array with 1 string)
  2. Values: ["value"] (Array with 1 string)
When new Function() receives that key array, it converts it to a string: "context, injected".
The Result:
We have successfully desynchronized the function signature from the data.
  • The function expects 2 arguments (context, injected).
  • Lodash only provides 1 value ("value").
This creates a "Gap" (hence the name 😁). The first argument (context) eats the value ("value"). The second argument (injected) is left starving—it receives undefined.

4. The Trigger: ES6 Default Parameters

In modern JavaScript, you can define default values for arguments. These execute only if the argument is undefined.
You must understand this sentence well, read it again
If we call test(undefined), the code executes.
We don't need to inject into the function body. We don't need to break out of a string context. We execute code during the argument initialization phase, before the function body even runs. Neat, right?

5. Exploit Strategy A: The "Pinned" Method

We can't just send a raw payload because Lodash is "helpful". It automatically appends its own library object (_) to the end of the imports.
If we blindly send our payload, the argument list looks like this: function(unused, payload, _)
And the values look like this: ["val", lodashLibraryObject]
The values slide over. unused gets "val". payload gets lodashLibraryObject. Since payload is not undefined, our code does not run.

The Solution: Pinning _

We explicitly include _ in our JSON to control the alignment.
The Payload:
The Execution Flow:
  1. Parsing Keys: Lodash sees keys: ["_", "unused, x = console.log('PWNED')"] JS Engine sees args: _, unused, x (plus the assignment code)
  2. Parsing Values: Lodash sees values: ["pinned", "val"]
  3. The Mapping (The Moment of Truth):
Function ArgumentValue ReceivedResult
_"pinned"Standard assignment
unused"val"Standard assignment
xundefinedRan out of values! Default param executes.

6. Exploit Strategy B: The "Over-Extension" Method

Alternatively, you can choose not to fight the default _ injection, but rather swallow it.
If we know Lodash will append _ to the keys and the library object to the values, we can structure our payload to "eat" that extra value and still come up short.
The Payload:
The Execution Flow:
  1. Parsing Keys:
    • User key: "a,b,c = ..."
    • Default key: "_" (Appended by Lodash)
    • Resulting Signature: function(a, b, c = ..., _)
  2. Parsing Values:
    • User value: "bruh"
    • Default value: lodashLibraryObject (Appended by Lodash)
    • Resulting Values: ["bruh", lodashLibraryObject]
  3. The Mapping:
Function ArgumentValue ReceivedResult
a"bruh"Filled by your value
blodashLibraryObjectFilled by the default _ "sliding" over
cundefinedRan out of values! Default param executes.
_undefinedIrrelevant
This method is arguably cleaner as it requires fewer keys in the JSON payload, but requires understanding that you are creating three slots (a, b, c) to consume two values ("bruh", lodashLib).

7. Final Payload

We combine everything. We use the comma injection to create the gap, the pinned _ to align the gap (using Strategy A)
Request:
What happens on the server:
  1. Express parses the JSON body.
  2. lodash takes the keys.
  3. new Function creates:
  4. The function is called with ("pinned", "val").
  5. _ = "pinned".
  6. unused = "val".
  7. x = undefined.
  8. Bingo!! The default parameter evaluates. The shell command runs.

That was it! These technical details were really fun to understand. Sadly, it was solved by AI by a lot of players, but at least now I know how to make new anti-AI challenges 😈😈. Stay tuned for more 💖
Tags:

You might also like

Pizza