0%

Nodejs以及原型链的知识

简介:什么是Nodejs

简要的介绍与例子

简单来说就是运行在服务端的JavaScript,基于Google的v8(真男人就要开v8)引擎,执行JavaScript的速度非常快,性能也很好(本博客也是采用了Nodejs)

可以自己装一下(不懂的看教程,或者学着搭个博客)

Nodejs启动http服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 引入 http 模块
const http = require('http');

// 创建服务器
const server = http.createServer((req, res) => {
// 设置响应头(告诉浏览器这是纯文本)
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });

// 输出内容
res.end('Hello Node.js HTTP Server!\n');
});

// 监听端口 3000
server.listen(3000, () => {
console.log('服务器已启动:http://localhost:3000');
});

然后再powershell中

1
node server.js

即可运行

大家可以去试试手搓的感觉

nodejs语言的缺点

大小写变换

1
2
3
4
toUpperCase(): 将小写转换为大写的函数

toLowerCase(): 将大写转换为小写的函数

前者可以将ı转换为I, 将 ſ转为为S
alt text
后者可以将İ转换为i, 将 K 转换为 k(这里的K只是一个类似大写K的自读,并非大写K,千万要注意)

nodejs弱类型比较

  1. 字符数字比较
1
2
3
4
5
6
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码之后再进行比较,因此就会出现console.log(‘111’>’3’)//false,而非数字型字符串与任何数字进行比较都是false

  1. 数组比较
1
2
3
4
5
console.log([]=[]);//false
console.log([]>[]);//false
console.log([]>[]);//false
console.log([6,2]>[5]);//true
console.log([100,2]<'test');//true

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

  1. 其他一些关键字的比较
1
2
3
4
console.log(null==underfined)//输出true
console.log(null==underfined)//输出false
console.log(NaN==NaN)//输出false
console.log(NaN===NaN)//输出false

nodejs:md5的绕过

1
a&&b&&a.length===b.length&&a!==b&&md5(a+flag)===md5(b+flag)

对于这个我们可以打

1
2
3
4
5
6
7
8
9
10
11
12
13
a={x:1}
>{x:1}

b={x:2}
>{x:2}

console.log(a+"test")
[object Object]test
undefined

console.log(b+"test")
[object Object]test
undefined

我们定义的对象会被解析为[object Object],然后就可以过强比较了

nodejs:编码绕过

  1. 16进制编码
1
console.log("a"==="\x61");//结果是true

实际上我们在nodejs中可以修改任意字符为其hex值,意义与结果都不变
接下来的两种方式也是一样

  1. unicode编码
1
console.log("\u0061"==="a");//true
  1. base64编码
1
console.log(Buffer.from("dGVzdA==",'base64').toString())

nodejs危险函数的利用

命令执行
  1. exec()
1
require('child_process').exec('open /System/Applications/Calculator.app');

调用模块然后调用函数,执行命令(如果是windows的话需要将open改成calc)
这里弹计算机啦,有黑客

  1. eval()
1
2
console.log(eval("document.cookie"));//执行document.cookie,输出当前页面的cookie信息
console.log("document.cookie"); //直接输出"document.cookie"这个字符串
文件读写
写文件
  1. writeFileSync()
1
require('fs').writeFileSync('input.txt','sss');

两个变量,文件名与写入的内容

  1. writeFile()
1
require('fs').writeFile('input.txt','test',(err)=>{});

三个变量,最后要写一个返回值,也就是(err)=>{}

读文件
  1. readFileSync()
1
2
3
4
require('fs').readFile('/etc/passwd','utf-8',(err,data)=>{
if (err) throw err;
console.log(data);
});

三个变量

  1. readFile()
1
require('fs').readFileSync('/etc/passwd','utf-8');
RCE bypass

这里只举一个简单的例子

编码绕过

原语句
exp

1
require("child_process").execSync('open /System/Application/Calculator.app/');

变形语句

1
2
3
4
5
require("child_process")['exe'%2b'cSync']('open /System/Application/Calculator.app/');//%2b就是+的url编码

require('child_process')["exe".concat("cSync")]('open /System/Application/Calculator.app/');

eval(Buffer.from('你想写的命令','base64').toString());
模板拼接

原语句

1
require("child_process").execSync('open /System/Applications/Calculator.app/');

变形语句

1
require("child_process")['${'${'exe'}cSync'}']('open /System/Applications/Calculator.app/');

总结

方法 示例 说明
exec require("child_process").exec("open /System/Applications/Calculator.app/"); 通过 shell 执行命令字符串(有 shell 注入风险)。
execSync require("child_process").execSync("open /System/Applications/Calculator.app/"); 同步版本,通过 shell 执行命令字符串。
execFile require("child_process").execFile("/usr/bin/open", ["/System/Applications/Calculator.app/"]); 直接调用可执行文件并传入参数数组(第二个参数传 args)。比 exec 更安全,不走 shell。
spawn require("child_process").spawn('open', ['/System/Applications/Calculator.app/']); 启动子进程并返回流式接口(异步、适合长时间运行的进程)。
spawnSync require("child_process").spawnSync('open', ['/System/Applications/Calculator.app/']); spawn 的同步版本。
execFileSync require("child_process").execFileSync('open', ['/System/Applications/Calculator.app/']); execFile 的同步版本,直接调用可执行文件并传参。

Nodejs ssrf (并不完善,稍后补充)

原理

用户发出的http请求通常将请求路径指定为字符串,但Node.js最终必须将请求作为原始字节输出。JavaScript支持unicode字符串,因此将它们转换为字节意味着选择并应用适当的unicode编码。对于不包含主体的请求,Node.js默认使用 “latin1”,这是一种单字节编码,不能表示高编号的unicode字符。相反,这些字符被截断为其JavaScript表示的最低字节

unicode转url编码

1
2
3
4
5
> http.get('http://example.con/\u010D\u010A/test')._header
'GET /%C4%8D%C4%8A/test HTTP/1.1\r\n' +
'Host: example.con\r\n' +
'Connection: keep-alive\r\n' +
'\r\n'

很明显的出现了\r,\n字符的截断

unicode解码

看如下图
alt text

出现了čĊ字样

通过拆分请求实现ssrf

当nodejs版本8或者更低版本对此url发出get请求时,结果字符串被编码为latin1写入路径时,这些字符分别被截断为”\r”与”\n”

1
Buffer.from('http://example.com/\u{010D}\u{010A}/test','latin1').toString()

返回的结果就是

1
http://example.con/\r\n/test

所以我们只要通过在请求路径中包含精心选择的unicode字符,攻击者可以欺骗nodejs将http协议控制字符写入路径

这个bug已经在nodejs10中被修复,如果请求路径包含非ascii字符,则会抛出错误

但是对于nodejs8或者更低版本,如果有下列情况,任何发出传出http请求的服务器可能受到通过请求拆实现ssrf的攻击

  1. 接受来自用户输入的unicode字符
  2. 并将其包含在http请求的路径中
  3. 且请求具有一个0长度的主体(如一个GET或者一个DELETE)

Nodejs原型链污染

这一块的基础部分,如果只是看我的内容,绝对是学不会的,我的语言表达能力并不很好,所以我会加上几篇我看的文章和视频的链接,基本上看完再自己打一遍poc是可以学会这块内容的

基础开发内容看这篇
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain
学习与理解强烈建议看这篇p牛的文章
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html#0x02-javascript
如果还是不懂要看图来理解构建函数,原型,类,以及对象之间的关系,花20分钟看这个视频
https://www.bilibili.com/video/BV1PG411774p/?spm_id_from=333.1387.favlist.content.click&vd_source=a71b7cc8978223fdf3236864e3621473

prototype原型

对于使用过基于类的语言(如 Java 或 C++)的开发者们来说,JavaScript 实在是有些令人困惑——JavaScript 是动态的,本身不提供一个 class 的实现。即便是在 ES2015/ES6 中引入了 class 关键字,但那也只是语法糖,JavaScript 仍然是基于原型的。
当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 proto)指向它的构造函数的原型对象(prototype)。该原型对象也有一个自己的原型对象(proto),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
几乎所有 JavaScript 中的对象都是位于原型链顶端的 object 的实例。
尽管这种原型继承通常被认为是 JavaScript 的弱点之一,但是原型继承模型本身实际上比经典模型更强大。例如,在原型模型的基础上构建经典模型相当简单。

1
2
3
4
5
function Son(){};
var son=new Son();
console.log(Son.prototype)//Son{}
console.log(son.__proto__)//Son{}
console.log(Son.prototype==son.__proto__)//true
1
2
3
4
5
6
7
8
graph TD
SonFunc[Son构造函数] -- prototype --> SonProto[Son原型对象]
SonProto -- constructor --> SonFunc
SonProto -- __proto__ --> ObjectProto[Object原型]
ObjectProto -- __proto__ --> null

instance[son实例] -- __proto__ --> SonProto
instance -- constructor --> SonFunc

看到这基本上需要的前置知识就结束了

然后就是继承链,也就是当前类的原型的属性是可以被当前类继承的,所以我们只要对原型的属性进行更改或者新加定义,那么当前类就可以继承我们需要打进去的污染的属性,这个就是原型链污染

这里举一个例子来看原型链污染
算了还是看p神的吧,感觉我讲的再怎么牛也不可能比p神要好了
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html?page=1#reply-list

这里注意两点

  1. payload的json格式
  2. merge操作与类merge操作一般都会存在原型链污染(不然你怎么能去更改原型的属性)

p牛就是p牛,有一种脑子被爆炒的感觉,豁然开朗