0x00 前言

因为在CTF中时常也会考察原型链污染的问题,以前也一直让我捉襟见肘,一直没有系统的学习了解过JS原型的这些相关概念,因此写下本文,通过不断总结大佬的文章,写出自己对于此部分内容的理解。同时建议学习本文前要有对面向对象部分知识的一定理解(无论哪种语言)。

JavaScript没有”子类”和”父类”的概念,也没有”类”(class)和”实例(instance)的区分,全靠一种很奇特的”原型链”(prototype chain)模式,来实现继承。 在javascript中一切皆对象,因为所有的变量,函数,数组,对象 都始于object的原型即object.prototype。

0x01 JavaScript原型

一、对象和函数

在学习原型和原型链之前,首先一定要搞清楚对象和函数到底有什么区别和联系:

“对象是由函数创建的,而函数又是一种对象。”这样一句话要深刻记忆。

我们都知道JavaScript可以在浏览器中使用“F12”打开的控制台中输入JavaScript代码进行执行,但也要知道其实在浏览器中已经内置了几个全局函数可供随时调用,如:Number()、String()、Boolean()、Object()等 。

在JavaScript中声明一种数据类型的变量时其实有以下两种方式,而第一种可以更直观的体现对象和函数之间的关系,但第二种在各种语言中都较为常用。

// 声明一个字符串类型的变量
var str = new String('hello');  // 使用String()函数来创建一个string对象
// 声明一个数值类型的变量
var num = new Number(123);     // 使用Number()函数来创建一个number对象
// 声明一个布尔类型的变量
var b = new Boolean(true);     // 使用Boolean()函数来创建一个boolean对象
// 声明一个字符串类型的变量
var str = 'hello';          // 用字符串字面量形式创建string对象
// 声明一个数值类型的变量
var num = 123;              // 用数值字面量形式创建number对象
// 声明一个布尔类型的变量
var b = true;               // 用布尔字面量形式创建boolean对象

​但第二种虽然我们是用赋值形式创建的,但在JavaScript的内部,则仍然是通过调用函数来创建对象的。也就是说他们是一样的。

而对于“函数又是一种对象”这句话,也可以使用 instanceof 关键字来验证:

instanceof 的作用是判断一个对象是不是一个函数的实例。
比如 obj instanceof fn
实际上是判断fn的prototype是不是在obj的原型链上。
比如:
obj.__proto__ === fn.prototype
obj. __proto__.__proto__=== fn.prototype
obj. __proto__ … __proto__ === fn.prototype
以上只要一个成立即可。

以上这个内容如果现在看不懂,不要着急后面会解释什么是原型、原型链和__proto__属性。

二、原型(共有属性):__proto__

首先定义个对象:var str = new String(‘hello’); 输出看看该对象中包含哪些属性:

再创建一个num对象。

可以看到两个不同的对象,但都存在__proto__属性。再看以下例子:

肯定会疑惑valueOf和toString方法是哪里来的呢,其实这两个方法也都是在__proto__属性中带来的,打开__proto__的指向箭头就可以看到

总结:不只是str和num对象,每个对象中都有__proto__属性,JavaScript将这些对象(如:Number(函数也是对象))中的共有属性,拿了出来,全都集中到一个新的对象(num)中。而新对象中,就保存着一个__proto__,指向这个原对象。

而__proto__所指向的这个原对象,也叫做原型对象。后文会继续解释。

而既然存在共有属性,那也一定存在独有属性。string对象有string对象的属性是其他对象没有的;number对象有number对象的属性是其他对象没的;boolean对象有boolean对象的属性是其他对象没有的;以此类推。 那这个“独有对象”又是保存在哪儿的呢?我们来看看什么是prototype。

三、函数的原型(prototype)

上面说到,__proto__是每个对象都有的属性,那么要区别记住的是prototype是函数才有的属性。

再继续了解prototype属性前再补充学习几个知识点:

1-什么是构造函数

Person就是一个构造函数,我们使用 new 创建了一个实例对象 person

2-constructor属性

接着按照刚刚的例子查看Person和person的结构输出。

可以看到person的构造函数Person存在的原型包含一个constructor属性。接下来记住一句话:“每个原型(prototype)都有一个 constructor 属性指向关联的构造函数,实例原型指向构造函数 ”。再看person的结果中__proto__属性所指的constructor属性也是与之关联的构造函数,而对于该例中,它的构造函数就是function Person()函数,因此整个结构图如下图所示。

所以以下代码是成立的

function Person() {
}
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true

0x02 JavaScript原型链

其实当认真理解完上面的内容,原型链的概念就基本清楚了,以下总结出几点:

1-从上面的代码中可以看到,创建person对象虽然使用的是由构造函数Person创建,但是对象创建出来之后,这个person对象其实已经与Person构造函数没有任何关系了,person对象的__proto__属性指向的是Person构造函数的原型对象(Person.prototype)。
2-如果使用new Person()创建多个对象person1、person2、person3,则多个对象都会同时指向Person构造函数的原型对象。
3-我们可以手动给这个原型对象添加属性和方法,那么person1、person2、person3这些对象就会共享这些在构造函数的原型对象中添加的属性和方法。
4-如果我们访问person中的一个属性name,如果在person对象中找到,则直接返回。如果person对象中没有找到,则直接去person对象的__proto__属性指向的原型对象中查找,如果查找到则返回。(如果原型中也没有找到,则继续向上找原型的原型—原型链),直到最高级Object的__proto__为Null为止。
5-如果通过person对象添加了一个属性name,则通过person访问name时,就相当于屏蔽了原型中的属性name,输出的是person对象中的name值
6-通过person对象只能读取构造函数的原型中的属性name值,而不能修改原型中的属性name值。 person.name = “purplet”; 并不是修改了原型中的值,而是在person对象中给添加了一个属性name。

下面可以把原型、原型链的关系当作一个公式一般去记忆:

var 对象 = new 函数()     
对象.__proto__ === 对象的构造函数.prototype

// 推论
var str = new String()      // 前面的str是对象,后面的String是函数
str.__proto__ === String.prototype()
String.__proto__ === Function.prototype   // 因为 String 是 Function 的实例

var number = new Number()    // 前面的number是对象,后面的Number是函数
number.__proto__ === Number.prototype
Number.__proto__ === Function.prototype // 因为 Number 是 Function 的实例

var object = new Object()     // 前面的object是对象,后面的Object是函数
object.__proto__ === Object.prototype
Object.__proto__ === Function.prototype // 因为 Object 是 Function 的实例

var function = new Function()     // 前面的function是对象,后面的Function是函数
function.__proto__ === Function.prototype
Function.__proto__ === Function.prototye // 因为 Function 是 Function 的实例!

由于__proto__是任何对象都有的属性,而JavaScript里万物皆对象,所以会形成一条__proto__连起来的链条,但递归访问__proto__必须最终到头,其终点是Null
当JavaScript引擎查找对象的属性时,先查找对象本身是否存在该属性,如果不存在,会在原型链上查找,但不会查找自身的prototype,如图所示。

0x03 JavaScript原型链污染

在看懂原型链的那几点内容后,其实就应该可以理解什么是原型链污染了,就是修改其构造函数的原型中的属性值,使其他通过该构造函数实例出的对象也具有该属性值。

可以看到我们修改成功了,新生成的 foo2 对象也具有hacker 属性,如果给foo1再往上加一个__proto__就可以修改(添加)Object的属性了。

那么在哪些情况下原型链会存在污染

这里我引用郁离歌师傅的博客内容了。

我们思考一下,哪些情况下我们可以设置__proto__的值呢?其实找找能够控制数组(对象)的“键名”的操作即可:

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

以对象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)

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)

可见,新建的o3对象,也存在b属性,说明Object已经被污染:

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

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

0x04 文章参考

https://www.zhihu.com/tardis/sogou/art/44035916

https://www.jianshu.com/p/be7c95714586

https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x04

HTTP完善

设置中文响应时的解析编码

//引入http核心模块
var http = require('http');
//创建一个服务
var server = http.createServer();
//绑定连接
server.on('request',function(res,rs){
    if(res.method == 'POST'){
        console.log('POST请求');
    }else if(res.method == 'GET'){
        console.log('GET请求');
    }
    rs.setHeader('Content-type','text/plain;charset=utf8');//设置解析编码
    rs.write('六日六日');//中文响应必须设置setHeader
    rs.end();//最后断开连接
});
//启动监听
server.listen(8081,function(){
    console.log('请访问127.0.0.1:8081');
})

响应HTML页面代码

//引入http核心模块
var http = require('http');
//创建一个服务
var server = http.createServer();
//绑定连接
server.on('request',function(res,rs){
    if(res.method == 'POST'){
        console.log('POST请求');
    }else if(res.method == 'GET'){
        console.log('GET请求');
    }
    rs.setHeader('Content-type','text/html;charset=utf8');//设置解析为HTML
    //rs.write('六日六日');//中文响应必须设置setHeader
    rs.end('<h1>六日</h1>');//相当于执行了write再end,同时识别html标签了
});
//启动监听
server.listen(8081,function(){
    console.log('请访问127.0.0.1:8081');
})

通过引入一个html页面,进行服务器响应

//引入http核心模块
var http = require('http');
//引入文件读取模块fs
var fs = require('fs');
//创建一个服务
var server = http.createServer();
//绑定连接
server.on('request',function(res,rs){
    if(res.method == 'POST'){
        console.log('POST请求');
    }else if(res.method == 'GET'){
        console.log('GET请求');
    }
    rs.setHeader('Content-type','text/html;charset=utf8');//设置解析为HTML
    fs.readFile('./index.html','utf-8',function(err,data){
        rs.end(data);//返回html代码
    })
});
//启动监听
server.listen(8081,function(){
    console.log('请访问127.0.0.1:8081');
})

​响应一个图片,首先准备好图片:在img/01.jpg

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<img src="./img/1.jpg" alt="">
</body>
</html>

​JS处调用

//引入http核心模块
var http = require('http');
//引入文件读取模块fs
var fs = require('fs');
//创建一个服务
var server = http.createServer();
//绑定连接
server.on('request',function(res,rs){
    var urls = res.url;
    if(urls == '/'){
        rs.setHeader('Content-type','text/html;charset=utf-8');
        fs.readFile('./01.html','utf-8',function(err,data){
            rs.end(data);
        })
    }else{
        fs.readFile('.'+urls,function(err,data){
            rs.end(data);
        })
    }
});
//启动监听
server.listen(8081,function(){
    console.log('请访问127.0.0.1:8081');
})

​服务器正常会请求三次的,第二次请求图片路径为:/img/01.jpg,所以在else处进行拼接一个.同时图片不需要进行编码处理

JSON相互转换

var arr = ['a','b','张三']
var str1 = JSON.stringify(arr);
console.log(str1);
console.log(typeof str1); 

注意虽然输出的格式与数组相同,但是实际已经是字符串类型了

var str2 = '{"name":"purplet","age":20}';
console.log(JSON.parse(str2));
console.log(typeof JSON.parse(str2)); 

通过JSON.parse()转换后的是一个对象


REPL环境运行js代码

浏览器的控制台和node的运行环境都属于REPL运行环境

模块

nodejs分为三个模块:核心模块,第三方模块,自定义模块

在nodejs的官网中提供的模块都是核心模块

https://nodejs.org/dist/latest-v12.x/docs/api/

读取文件内容

var fs = require('fs');//引入核心的fs模块
fs.readFile('./hello.txt','utf8',function(err,data){
    console.log(err);
    console.log('------------');
    console.log(data);
});
console.log(1111);

​ fs此时就相当于一个对象 ,可以调用fs模块中的所有方法,readFile有三个参数fs.readFile(path[, options], callback),可参看官方手册:http://nodejs.cn/api/fs.html#fs_fs_readfile_path_options_callback

该方法同时是一个异步操作,当整块fs.readFile运行完后fs模块才会去根据传递的相对路径寻找hello.txt,然后将结果通过回调函数的打印输出回来

写入文件内容

var fs = require('fs');//引入核心的fs模块

fs.writeFile('./hello.txt','purplet',function(err){//第二个参数是写入文件内容,默认覆盖写入,回调函数只有一个err参数
    if(!err){
        console.log('写入成功');
    }
});

追加写入

首先读取文件内容再将数据内容叠加重新写入来实现追加写入。

var fs = require('fs');//引入核心的fs模块

fs.readFile('./hello.txt','utf8',function(err,data){
    data += ' like NodeJS';//先读取再将数据写入
    fs.writeFile('./hello.txt',data,function(err){
        if(!err){
            console.log('追加写入成功');
        }
    })
})

以上所有的核心模块引入都是使用require进行,当遇到没使用过的模块学会利用手册进行学习。

异步

通过一个ajax异步理解:首先新建一个01.html和一个2.php,其中2.php中写一个echo “222”;

01.html的内容:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input type="button" value="点击" id="btn">      
</body>
<script>
    document.getElementById('btn').onclick=function(){
        var xhr =new XMLHttpRequest();//得到一个ajax对象
        xhr.onreadystatechange=function(){   //onreadystatechange监听一个状态改变的事件
            if(xhr.readyState == 4){         //等于4代表服务器已经将全部数据返回
                alert(xhr.responseText);
            }
        }
        xhr.open('get','./2.php');//第三个参数不写默认异步
        xhr.send();//发送ajax的一个http请求

        alert(3);//会先执行这个弹窗,因为服务器返回的数据还没完
    }
</script>
</html>

开启HTTP服务

 //引入http核心模块
var http = require('http');
//创建一个服务
var server = http.createServer();
//绑定连接
server.on('request',function(res,rs){
    console.log(res.method);//打印请求的方法
    rs.write('nihao');//返回数据
    rs.end();//断开连接
})
//启动监听
server.listen(8081,function(){
    console.log('请访问127.0.0.1:8081');
})

启动后访问127.0.0.1:8081可以看到下图所示,一个最简单的HTTP服务器就搭建好了。

NPM

npm其实是Node.js的包管理工具(package manager)。

为啥我们需要一个包管理工具呢?因为我们在Node.js上开发时,会用到很多别人写的JavaScript代码。如果我们要使用别人写的某个包,每次都根据名称搜索一下官方网站,下载代码,解压,再使用,非常繁琐。于是一个集中管理的工具应运而生:大家都把自己开发的模块打包后放到npm官网上,如果要使用,直接通过npm安装就可以直接用,不用管代码存在哪,应该从哪下载。

更重要的是,如果我们要使用模块A,而模块A又依赖于模块B,模块B又依赖于模块X和模块Y,npm可以根据依赖关系,把所有依赖的包都下载下来并管理起来。否则,靠我们自己手动管理,肯定既麻烦又容易出错。

讲了这么多,npm究竟在哪?

其实npm已经在Node.js安装的时候顺带装好了。我们在命令提示符或者终端输入npm -v,应该看到类似的输出:

六大数据类型

字符串、对象(数组,object,function函数)、null、undefined、数值、布尔

null和undefined的区别

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="div">purplet</div>
</body>
<script>
    var a;//undefined
    var b = document.getElementById('xxxx');
    console.log(b);
</script>
</html>

undefined出现:
1.变量声明但没有赋值
2.获取对象中不存在的属性
3.函数有形参没有传实参,形参的值是undefined
4.函数内部没有return或return后没有任何东西时调用的值是undefined

null出现:
对象不存在

null的数据类型是object,但null并不是object类型

IF条件:

    if(1){

    }else if(){

    }else{
        
    }

循环

    for(1;1;1;){

    }
    while(1){

    }
    do{

    }while(1);

数组

var arr = [1,2];
var arr = new Array();//使用内置构造函数
var arr = ['a'=>1,'b'=>2];//JS中没有这种形式,PHP里称为关联数组

for循环打印数组内容

for (var i=1;i<=arr.length;i++){
    console.log(arr[i])
}
for (i in arr){
    console.log(arr[i]);
}
function f(value,index,array) {
        console.log("a[" + index + "] = " + value);
        console.log(array);
    }
var a = ['a', 'b', 'c'];
a.forEach(f); 

第三种利用内置函数forEach,进行回调函数打印输出

对象

var obj1 = {name:'purplet',age:20,fun:function(){
        alert(222);
}};
console.log(obj1.name);
console.log(obj1.fun);//只打印这个函数
console.log(obj1.fun());//运行函数内代码,弹窗后返回undefined(函数没有返回值)
//其中name,age,fun都是这个对象的属性,

函数声明

f1();//报错,表达式的必须先声明才可引用
f2();//正常执行
//表达式声明
var f1 = function{

}
//直接量
var f2(){

}
//内置构造函数(基本不用)
var f3 = new Function();

再来一个重要知识点

    var a=1;//全局变量
    function f1(f2){
        var a=2;//局部变量
        f2();
    }
    //在JS中函数自身的作用域在声明的地方,不在调用的地方
    function f2(){
        console.log(a);
    }
    f1(f2);

输出结果是1,请大家仔细理解。

闭包

    var a = 2;
function f1(f2){
var a = 1;
function f2(){
console.log(++a);
}
//由于f1的运行结果是返回f2
//又由于函数作用域链和函数自身作用域的问题
//导致f1运行结束后不能销毁变量
//因此形成了闭包
return f2;
}
var f = f1();
f();
f();
f();

输出2 3 4

面向对象

在JS中父级对象称为原型,Object是所有对象的的父级对象,理论上Object是没有父级对象的,然而Object的父级对象的值是Null(可以理解为无中生有)

原型链:每一个对象都有父级对象,而父级对象就称为原型。

如果在低级对象找一个属性找不到时会向父级对象中寻找,依次向上

    var s = '1';
    var o = [];
    var p = {};
    function f(){

    }
    console.log(p.__proto__);//object
    console.log(o.__proto__.__proto__);//object

__proto__是寻找该对象的父级对象

0x00 介绍

Node.js是一个Javascript 运行环境(runtime)。它让JavaScript可以开发后端程序,它几乎能实现其他后端语言能实现的所有功能。

Nodejs是基于GoogleV8引擎,v8引擎是Google发布的一款开源的JavaScript引擎,原来主要用于Chrome浏览器的Js解释部分,但是Ryan Dahl这哥们,鬼才般的,把这个V8

Nodejs 最擅长高并发:Nodejs最擅长的就是处理高并发,在Java、PHP或者.net等服务器端语言中,会为每一个客户端连接创建一个新的线程。而每个线程需要耗费大约2MB内存。也就是说,理论上,一个8GB内存的服务器可以同时连接的最大用户数为4000个左右。要让Web应用程序支持更多的用户,就需要增加服务器的数量,而Web应用程序的硬件成本当然就上升了。Node.js不为每个客户连接创建一个新的线程,而仅仅使用一个线程。当有用户连接了,就触发一个内部事件,通过非阻塞I/0、事件驱动机制,让Node.js程序宏观上也是并行的。使用Node.is,一个8GB内存的服务器,可以同时处理超过4万用户的连接。

Nodejs可实现的功能多:Nodejs不仅可以像其他后端语言一样写动态网站、写接口,还可以应用在云计算平台、游戏开发、区块链开发、即时通讯、跨平App开发、桌面应用开发(electron)、云直播、物联网领域等。

0x01 安装

下载地址:https://nodejs.org/en/download/,根据电脑位数及操作系统对应下载

一路默认下一步即可

最后在cmd窗口中输入node -v,出现版本信息即可

NodeJS搭配使用Vscode编辑器,如下所示创建,输入代码

如下选择进入命令行窗口,但是第一次配置出现下图错误

找到VScode处右键属性,如下图选择

应用后确定,重新打开VScode,再次运行即可成功