NodeJS
NodeJS
CH0ico0x01 Node.JS 基础
1.1 简单介绍
Node.js 就是运行在服务端的 JavaScript
Node.js 是一个基于Chrome JavaScript 运行时建立的一个平台
Node.js 是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好
1.2 基础漏洞
1.2.1 大小写特性
1 | toUpperCase() |
1.2.2 弱类型
1. 大小比较
1 | // 数字与字符串 |
- 数字与字符串比较时,会优先将纯数字型字符串转为数字之后再进行比较
- 字符串与字符串比较时,会将字符串的第一个字符转为ASCII码之后再进行比较
- 非数字型字符串与任何数字进行比较都是false
- 空数组之间比较永远为false
- 数组之间比较只比较数组间的第一个值,对第一个值采用前面总结的比较方法,数组与非数值型字符串比较,数组永远小于非数值型字符串
- 数组与数值型字符串比较,取第一个之后按前面总结的方法进行比较
2. 相等
1 | console.log(null==undefined) // 输出:true |
3. 拼接
1 | console.log(5+[6,6]); //56,3 |
1.2.3 MD5 绕过
1 | a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag) |
数组会被解析成[object Object]
1 | a={'2':'1'} |
1.2.4 编码绕过
16进制编码
1 | console.log("a"==="\x61"); // true |
unicode编码
1 | console.log("\u0061"==="a"); // true |
base编码
1 | eval(Buffer.from('Y29uc29sZS5sb2coImhhaGFoYWhhIik7','base64').toString()) |
1.3 函数利用
1.3.1 命令执行
1. exec( )
1 | require('child_process').exec('open /System/Applications/Calculator.app'); |
2. eval( )
1 | require('child_process').execSync('ls').toString(); |
1.3.2 nodejs危险函数-文件读写
1. 读
readFileSync()
1 | require('fs').readFile('/etc/passwd', 'utf-8', (err, data) => { |
readFile()
1 | require('fs').readFileSync('/etc/passwd','utf-8') |
2. 写
writeFileSync()
1 | require('fs').writeFileSync('input.txt','sss'); |
writeFile()
1 | require('fs').writeFile('input.txt','test',(err)=>{}) |
1.3.3 RCE bypass
bypass
1. 原型:
1 | require("child_process").execSync('cat flag.txt') |
2. 字符拼接:
1 | require("child_process")['exe'%2b'cSync']('cat flag.txt') |
3. 编码绕过:
1 | require("child_process")["\x65\x78\x65\x63\x53\x79\x6e\x63"]('cat flag.txt') |
4. 模板拼接:
1 | require("child_process")[`${`${`exe`}cSync`}`]('open /System/Applications/Calculator.app/') |
其他函数:
1 | require("child_process").exec("sleep 3"); |
1.4 nodejs中的ssrf
通过拆分攻击实现的SSRF攻击
request splitting
1.4.1 成因
虽然用户发出的 http 请求通常将请求路径指定为字符串,但Node.js 最终必须将请求作为原始字节输出。
JavaScript支持 unicode 字符串,因此将它们转换为字节意味着选择并应用适当的unicode编码。对于不包含主体的请求,Node.js默认使用“latin1”,这是一种单字节编码,不能表示高编号的unicode字符。相反,这些字符被截断为其JavaScript表示的最低字节
1.4.2 拆分请求
如果服务器未正确验证用户输入,则攻击者可能会直接注入 协议控制字符
到请求里。假设在这种情况下服务器接受了以下用户输入:
1 | "x HTTP/1.1\r\n\r\nDELETE /private-api HTTP/1.1\r\n" |
接收服务将此解释为两个单独的HTTP请求,一个GET
后跟一个DELETE
,它无法知道调用者的意图。
实际上,这种精心构造的用户输入会欺骗服务器,使其发出额外的请求,“SSRF”。
服务器可能拥有攻击者不具有的权限,例如访问内网或者秘密api密钥
通过拆分攻击实现的SSRF攻击 - 先知社区 (aliyun.com)
1.4.3 SSRF 漏洞
上述的处理unicode字符错误意味着可以规避这些措施。考虑如下的URL,其中包含一些带变音符号的unicode字符:
1 | > 'http://example.com/\u{010D}\u{010A}/test' |
当Node.js版本8或更低版本对此URL发出GET
请求时,它不会进行转义,因为它们不是HTTP控制字符:
1 | > http.get('http://example.com/\u010D\u010A/test').output |
但是当结果字符串被编码为latin1写入路径时,这些字符将分别被截断为“\r”和“\n”:
1 | > Buffer.from('http://example.com/\u{010D}\u{010A}/test', 'latin1').toString() |
因此,通过在请求路径中包含精心选择的unicode字符,攻击者可以欺骗Node.js将HTTP协议控制字符写入线路。
这个bug已经在Node.js10中被修复,如果请求路径包含非ascii字符,则会抛出错误。但是对于Node.js8或更低版本,如果有下列情况,任何发出传出HTTP请求的服务器都可能受到通过请求拆实现的SSRF的攻击:
- 接受来自用户输入的unicode数据
- 并将其包含爱HTTP请求的路径中
- 且请求具有一个0长度的主体(比如一个
GET
或者DELETE
)
0x02 nodejs原型链污染
浅析CTF中的Node.js原型链污染 - FreeBuf网络安全行业门户
2.1 原型链
当谈到继承时,JavaScript 只有一种结构:对象。
每个对象 object
都有一个私有属性指向另一个名为 原型 prototype
的对象。
原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null
。根据定义,null
没有原型,并作为这个 原型链 prototype chain
中的最后一个环节。可以改变原型链中的任何成员,甚至可以在运行时换出原型
2.1.1 基于原型链的继承
JavaScript 对象是动态的属性包
JavaScript 对象有一个指向一个原型对象的链
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
1. 属性继承
1 | const o = { |
2. 方法继承
在 JavaScript 中,任何函数都被可以添加到对象上作为其属性。函数的继承与其他属性的继承没有差别
当继承的函数被调用时,this
值指向的是当前继承的对象,而不是拥有该函数属性的原型对象
1 | const parent = { |
2.1.2 构造函数
1 | class Box { |
2.1.3 构建更长的继承链
Constructor.prototype
属性将成为构造函数实例的 [[Prototype]]
,包括 Constructor.prototype
自身的 [[Prototype]]
。默认情况下,Constructor.prototype
是一个_普通对象_——即 Object.getPrototypeOf(Constructor.prototype) === Object.prototype
。
唯一的例外是 Object.prototype
本身,其 [[Prototype]]
是 null
——即 Object.getPrototypeOf(Object.prototype) === null
。因此,一个典型的构造函数将构建以下原型链:
1 | function Constructor() {} |
要构建更长的原型链,我们可用通过 Object.setPrototypeOf()
函数设置 Constructor.prototype
的 [[Prototype]]
。
1 | function Base() {} |
在类的术语中,这等同于使用 extends
语法。
1 | class Base {} |
你可能还会看到一些使用 Object.create()
来构建继承链的旧代码。然而,因为这会重新为 prototype
属性赋值并删除 constructor
属性,所以更容易出错,而且如果构造函数还没有创建任何实例,性能提升可能并不明显。
2.2 原型链污染
2.2.1 __ proto__ 和 prototype
在 JavaScript
中,每个对象都有一个名为 __proto__
的内置属性,它指向该对象的原型。同时,每个函数也都有一个名为 prototype
的属性,它是一个对象,包含构造函数的原型对象应该具有的属性和方法。简单来说,__proto__
属性是指向该对象的原型,而 prototype
属性是用于创建该对象的构造函数的原型(类)
prototype
是类Person
的一个属性,所有用类Person
进行实例化的对象,都会拥有prototype
的全部内容。
我们实例化出来的person1
对象,它是不能通过prototype
访问原型的,但通过__proto__
就可以实现访问Person
原型
2.2.2 污染
在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是 原型链污染
利用 copy
, merge
等函数可以导致原型链污染
1 | function merge(target, source) { |
2.2.3 原型链污染配合RCE
有原型链污染的前提之下,我们可以控制基类的成员,赋值为一串恶意代码,从而造成代码注入。
1 | let foo = {bar: 1} |
0x03 vm沙箱逃逸
vm是用来实现一个沙箱环境,可以安全的执行不受信任的代码而不会影响到主程序。但是可以通过构造语句来进行逃逸:
1 | const vm = require("vm"); |
执行之后可以获取到主程序环境中的环境变量
vm
模块:Node.js 的vm
模块提供了一个沙箱环境来执行代码。这使得你可以在受限的环境中运行脚本。创建脚本:
javascript复制代码
const script = new vm.Script("this.constructor.constructor('return this.process.env')()");
new vm.Script(...)
创建一个新的Script
实例,这里的脚本字符串是"this.constructor.constructor('return this.process.env')()"
。this.constructor.constructor
访问了Function
构造函数,这是一个绕过普通沙箱限制的技巧。正常情况下,这种方式可以用来执行任意代码。
脚本运行上下文:
javascript复制代码
const context = vm.createContext(sandbox);
vm.createContext(sandbox)
创建了一个新的沙箱上下文,sandbox
是一个空对象。这个上下文提供了代码执行的环境。
执行脚本:
javascript复制代码
env = script.runInContext(context);
script.runInContext(context)
在上面创建的沙箱上下文中执行脚本。- 脚本的内容
this.constructor.constructor('return this.process.env')()
通过Function
构造函数创建了一个新的函数,该函数返回this.process.env
。
脚本解析:
this
在沙箱上下文中指向sandbox
对象。this.constructor
是sandbox
对象的构造函数,通常指向Object
。this.constructor.constructor
则是Function
构造函数。Function
构造函数允许执行传入的字符串作为代码。('return this.process.env')()
创建了一个新的函数并立即执行它,返回this.process.env
。
输出环境变量:
javascript复制代码
console.log(env);
- 最终,
console.log(env)
打印出process.env
,即当前运行环境的所有环境变量。
- 最终,
上面例子的代码等价于如下代码:
1 | const vm = require('vm'); |
创建vm环境时,首先要初始化一个对象 sandbox,这个对象就是vm中脚本执行时的全局环境context,vm 脚本中全局 this 指向的就是这个对象
因为this.constructor.constructor
返回的是一个Function constructor
,所以可以利用Function对象构造一个函数并执行。(此时Function对象的上下文环境是处于主程序中的) 这里构造的函数内的语句是return this.process.env
,结果是返回了主程序的环境变量
配合chile_process.exec()
就可以执行任意命令了
1 | const vm = require("vm"); |
mongo-express RCE(CVE-2019-10758)漏洞就是配合vm沙箱逃逸来利用的。
具体分析可参考:CVE-2019-10758:mongo-expressRCE复现分析