Given the breadth of the npm universe and the Checkmarx Security Research Team’s always-on curiosity into performing investigations into open source projects and uncovering 0-days, we recently conducted an npm-focused vulnerability workshop. As a result of our efforts, we discovered an interesting remote code execution (RCE) deserialization issue in the npm Replicator package.
Deserialization of any untrusted input in the npm Replicator package, which sees more than 200,000 downloads per week, could lead to remote code execution and full compromise of the machine.
- Typed arrays
First, let's get a general idea of how Replicator serializes and deserializes those unsupported JSON objects. When trying to serialize a Set object for example, the following output will be generated:
The object Set was changed to a valid JSON with the key “@t” pointing to the type of the object inside the double brackets “[[type]]”, and the parameters passed to the object are under the “data” property.
When researching these types of packages, it’s interesting to focus on the deserialization component since it is more likely that a user input would be string passed to the deserialization code rather than an object passing to the serialization code.
After checking how the deserialization is implemented in the code, the first 8 types seemed safe. However, with further examination, we came across an issue at the last object type – TypeArray.
The function on the bottom is the one responsible for the deserialization. With that, let’s serialize one of the TypeArrays – Int8Array:
The property “ctorName” states the object name of the TypeArray and the “arr” is the object value. But the issue in the code is that there is no validation that the object name in “ctorName” is actually a TypeArray object. When serializing, on the other hand, there is a validation using the list “TYPED_ARRAY_CTORS” defined earlier.
Looking at the code shown before we can invoke “new” on every function under the GLOBAL object with our own parameters. Something like this:
From here, there are limitations that we need to bypass in order to achieve the code execution.
1 - From the “fromSerializable” function check, the “ctorName” must be a name of a function.
2 - The function must be a constructor.
a. Hence, we can’t just call “Eval” because it’s not a constructor.
3 - Must be a direct descendent of the GLOBAL object.
a. It’s impossible to call a function like “child_process.exec(‘evilcode’)” because we control only the value inside the brackets
4 - Must be a valid JSON.
a. As mentioned before, Replicator wraps around JSON parse and stringify. Due to that, the input string given to the decode function gets “JSON.parse” before going into the problematic “fromSerializable” function. So, calling “setImmediate” / “setInterval” / “setTimeout” isn’t possible because it requires a callback function as an input and that isn’t JSON valid.
The following payload will fail at JSON.parse before invoking a new setTimeout:
At this point, it looks like we cannot go further, but there are still some security concerns and vulnerabilities despite the limitations:
- Calling an arbitrary function created by the application using replicator, which heavily depends on the application and the specific situation, something like:
- Local file inclusion using “require” could lead to other vulnerabilities, such as: XSS, RCE, sensitive information disclosure, and more. This, as well, depends on the attack scenario.
After fuzzing and some more research, we asked ourselves what happens if we serialize a Set within a Set. What would that look like? Is it done recursively?
When understanding that the serialization/deserialization is done recursively, the payload to an RCE was around the corner. As mentioned before, we can create a new “Function”, but nothing will run it, and we can call “setTimeout,” but have to give it a function to execute.
Combining these two, here is the final payload to trigger code execution:
The inner object will create a function with the code input as a string, and the other object, "setTimeout", receives the function as an argument and runs the code.
Depends on the scope of the program, the shell exploit code could change. For example, in the picture above, the payload was:
because it was run in a REPL console.
To avoid issues like this, update the npm Replicator package to version 1.0.4 or later.
Summary of Disclosure and Events
After discovering and validating the vulnerabilities, we notified npm of our findings and worked with them throughout the remediation process until they informed us the issues were appropriately patched. NPM's responsiveness and professionalism throughout the process are commendable.
Timeline of Disclosure
- March 24, 2021: Vulnerability was reported responsibly
- March 24, 2021: Checkmarx SCA customers using npm and Replicator were warned and provided mitigation guidance, without exposing the technical details of the findings
- May 14, 2021: Pull request to fix the issue was created
- May 17, 2021: Fixed version 1.0.4 was released on NPM
- May 17, 2021: Advisory with the full details was published on the Checkmarx advisory website
- December 15, 2022: CVE-2021-33420 published
Discovering vulnerabilities like the ones documented in this report is why the Checkmarx Security Research Team performs investigations into open source projects. With open source making up the vast majority of today’s commercial software, security vulnerabilities must be taken seriously and handled carefully across the industry.
Solutions like Checkmarx SCA are essential in helping organizations identify, prioritize, and remediate open source vulnerabilities more efficiently to improve their overall software security risk posture. Checkmarx SCA customers receive notice of issues like the ones described above in advance of public disclosure. For more information or to speak to an expert about how to detect, prioritize, and remediate open source risks in your code, contact us.