简介:什么是Nodejs
简要的介绍与例子
简单来说就是运行在服务端的JavaScript,基于Google的v8(真男人就要开v8)引擎,执行JavaScript的速度非常快,性能也很好(本博客也是采用了Nodejs)
可以自己装一下(不懂的看教程,或者学着搭个博客)
Nodejs启动http服务
1 | // 引入 http 模块 |
然后再powershell中
1 | node server.js |
即可运行
大家可以去试试手搓的感觉
nodejs语言的缺点
大小写变换
1 | toUpperCase(): 将小写转换为大写的函数 |
前者可以将ı转换为I, 将 ſ转为为S
后者可以将İ转换为i, 将 K 转换为 k(这里的K只是一个类似大写K的自读,并非大写K,千万要注意)
nodejs弱类型比较
- 字符数字比较
1 | console.log(1=='1');//true |
数字与字符串进行比较的时候,会优先把纯数字型字符串转为数字转换再进行比较;而字符串与字符串比较时,会将字符串的第一个字符转为ascii码之后再进行比较,因此就会出现console.log(‘111’>’3’)//false,而非数字型字符串与任何数字进行比较都是false
- 数组比较
1 | console.log([]=[]);//false |
空数组之间的比较永远为false
数组之间比较只比较数组间的第一个值,对第一个值采用字符数字的比较方法
数组与非数值型字符串比较,数组永远小于非数值型字符串
数组与数值型字符串比较,取第一个之后按字符数字的比较方法进行比较
- 其他一些关键字的比较
1 | console.log(null==underfined)//输出true |
nodejs:md5的绕过
1 | a&&b&&a.length===b.length&&a!==b&&md5(a+flag)===md5(b+flag) |
对于这个我们可以打
1 | a={x:1} |
我们定义的对象会被解析为[object Object],然后就可以过强比较了
nodejs:编码绕过
- 16进制编码
1 | console.log("a"==="\x61");//结果是true |
实际上我们在nodejs中可以修改任意字符为其hex值,意义与结果都不变
接下来的两种方式也是一样
- unicode编码
1 | console.log("\u0061"==="a");//true |
- base64编码
1 | console.log(Buffer.from("dGVzdA==",'base64').toString()) |
nodejs危险函数的利用
命令执行
- exec()
1 | require('child_process').exec('open /System/Applications/Calculator.app'); |
调用模块然后调用函数,执行命令(如果是windows的话需要将open改成calc)
这里弹计算机啦,有黑客
- eval()
1 | console.log(eval("document.cookie"));//执行document.cookie,输出当前页面的cookie信息 |
文件读写
写文件
- writeFileSync()
1 | require('fs').writeFileSync('input.txt','sss'); |
两个变量,文件名与写入的内容
- writeFile()
1 | require('fs').writeFile('input.txt','test',(err)=>{}); |
三个变量,最后要写一个返回值,也就是(err)=>{}
读文件
- readFileSync()
1 | require('fs').readFile('/etc/passwd','utf-8',(err,data)=>{ |
三个变量
- readFile()
1 | require('fs').readFileSync('/etc/passwd','utf-8'); |
RCE bypass
这里只举一个简单的例子
编码绕过
原语句
exp
1 | require("child_process").execSync('open /System/Application/Calculator.app/'); |
变形语句
1 | require("child_process")['exe'%2b'cSync']('open /System/Application/Calculator.app/');//%2b就是+的url编码 |
模板拼接
原语句
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 | > http.get('http://example.con/\u010D\u010A/test')._header |
很明显的出现了\r,\n字符的截断
unicode解码
看如下图
出现了čĊ字样
通过拆分请求实现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的攻击
- 接受来自用户输入的unicode字符
- 并将其包含在http请求的路径中
- 且请求具有一个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 | function Son(){}; |
1 | graph TD |
看到这基本上需要的前置知识就结束了
然后就是继承链,也就是当前类的原型的属性是可以被当前类继承的,所以我们只要对原型的属性进行更改或者新加定义,那么当前类就可以继承我们需要打进去的污染的属性,这个就是原型链污染
这里举一个例子来看原型链污染
算了还是看p神的吧,感觉我讲的再怎么牛也不可能比p神要好了
https://www.leavesongs.com/PENETRATION/javascript-prototype-pollution-attack.html?page=1#reply-list
这里注意两点
- payload的json格式
- merge操作与类merge操作一般都会存在原型链污染(不然你怎么能去更改原型的属性)
p牛就是p牛,有一种脑子被爆炒的感觉,豁然开朗